问题
如何在子线程中通过主线程的Handler向主线程发布任务,然后等待主线程将任务处理完成以后再继续执行子线程?
Handler#runWithScissors
Handler源码中提供了方法runWithScissors就是用来处理上面问题的,但是开发者无法调用,因为被标记为@hide了,但是实现方法可以借鉴。
public final boolean runWithScissors(@NonNull Runnable r, long timeout) {
if (r == null) {
throw new IllegalArgumentException("runnable must not be null");
}
if (timeout < 0) {
throw new IllegalArgumentException("timeout must be non-negative");
}
if (Looper.myLooper() == mLooper) {
r.run();
return true;
}
BlockingRunnable br = new BlockingRunnable(r);
return br.postAndWait(this, timeout);
}
可以看到runWithScissors的参数有两个。参数Runnable指定需要执行的任务,后面的参数是设置超时时间。如果当前线程和Handler对应的线程是同一个线程则直接执行run方法。如果线程不一致,则会执行BlockingRunnable#postAndWait方法。
private static final class BlockingRunnable implements Runnable {
private final Runnable mTask;
private boolean mDone;
public BlockingRunnable(Runnable task) {
mTask = task;
}
@Override
public void run() {
try {
mTask.run();
} finally {
synchronized (this) {
mDone = true;
notifyAll();
}
}
}
public boolean postAndWait(Handler handler, long timeout) {
if (!handler.post(this)) {
return false;
}
synchronized (this) {
if (timeout > 0) {
final long expirationTime = SystemClock.uptimeMillis() + timeout;
while (!mDone) {
long delay = expirationTime - SystemClock.uptimeMillis();
if (delay <= 0) {
return false; // timeout
}
try {
wait(delay);
} catch (InterruptedException ex) {
}
}
} else {
while (!mDone) {
try {
wait();
} catch (InterruptedException ex) {
}
}
}
}
return true;
}
}
BlockingRunnable其实就是将发送给Handler的Runnable进行包装了一下。
postAndWait方法逻辑很简单,首先会通过Handler将BlockingRunnable发送出去,如果Handler对应的消息队列不存在或者Looper已经退出,则任务发送失败,直接返回false。否则就会对BlockingRunnable对象进行上锁,然后进入到循环(循环条件是任务是否完成),如果有超时时间,则会判断是否在指定的超时时间内任务没有结束,如果超时任务还没有结束则会直接返回false。如果没有超时,则调用wait(long timeout)方法阻塞发送任务的线程(子线程)。如果没有超时时间直接调用wait()方法阻塞发送任务的线程(子线程)。
run方法在Handler对应的线程执行的时候会调用发送任务的Runnable的run方法执行任务,执行结束以后会将用于死循环的标志位mDone置为true并调用notifyAll唤醒发送任务的线程,然后发送任务的线程(子线程)会退出循环。
wait和notify/notifyAll
JVM会为使用synchronized加锁的对象维护两个集合,Entry Set和Wait Set。
如果某个线程已经持有了对象锁,此时如果有其他线程也想获得该对象锁的话,就只能进入Entry Set,并且处于线程的BLOCKED状态。当对象锁被释放的时候,JVM会唤醒处于Entry Set中的某个线程,这个线程的状态会从BLOCKED转变为RUNNABLE。
如果线程调用了wait()方法,那么线程会释放该对象的锁,进入到Wait Set,并且处于线程的WAITING状态。当对象的notify()方法被调用时,JVM会唤醒处于Wait Set中的某个线程,这个线程的状态就从WAITING转变为RUNNABLE;或者当notifyAll()方法被调用时,Wait Set中的全部线程会转变为RUNNABLE状态。所有Wait Set中被唤醒的线程会被转移到Entry Set中。
每当对象的锁被释放后,那些所有处于RUNNABLE状态的线程会共同去竞争获取对象的锁,最终会有一个线程真正获取到对象的锁,而其他竞争失败的线程继续在Entry Set中等待下一次竞争机会。
为什么wait方法要放到循环中?
因为调用wait()的线程永远不能确定其他线程会在什么状态下notify(),所以必须在被唤醒、抢占到锁并且从wait()方法退出的时候再次进行特定条件的判断,如果满足条件则继续往下执行,如果不满足条件则继续等待。
弊端
- 超时无法取消
从上面的源码可以看出来,超时以后,直接返回false退出了,但是对应的任务还在Handler的消息队列中,没有被移除掉,最终任务还是会被执行。
- 死锁
如果通过Handler发送任务以后,通过Looper.quit退出消息循环,会清理未执行的任务,此时通过Handler发送任务的线程永远得不到唤醒,如果线程还持有其他锁,还会造成死锁。所以为了安全起见,发送任务以后,不允许Handler对应的Looper调用quit退出,或者使用quitSafely方法安全退出,因为quit会清理消息队列中所有消息,而quitSafely只会清理当前时间点之后的消息,之前的消息会保留,任务依然可以得到执行。