Android 线程等待的正确姿势:避免主线程阻塞

本文旨在帮助 Android 开发者理解在多线程环境下,特别是涉及到 UI 线程时,如何正确地处理线程等待问题。重点强调了避免在主线程中使用 `wait()` 或 `join()` 方法,以及可能导致的 UI 冻结和应用无响应问题。同时,提供了一种替代方案,即通过后台线程处理耗时操作,并使用加载界面或其他机制来通知用户操作状态。

在 Android 开发中,正确地管理线程是至关重要的,尤其是在处理耗时操作时。直接在主线程(UI 线程)执行耗时操作会导致应用卡顿,甚至崩溃。因此,开发者通常会将这些操作放在后台线程中执行。然而,如何让一个线程等待另一个线程完成,以及如何避免阻塞主线程,是需要认真考虑的问题。

避免在主线程中使用 wait() 和 join()

wait() 和 join() 是 Java 中用于线程同步的机制。wait() 方法使当前线程进入等待状态,直到其他线程调用 notify() 或 notifyAll() 方法唤醒它。join() 方法则使当前线程等待,直到被 join() 的线程执行完毕。

关键点:永远不要在主线程中使用 wait() 或 join()。

如果在主线程中调用 wait() 或 join(),会导致 UI 线程阻塞,用户界面无法响应,最终导致应用无响应(ANR)。Android 系统会检测到这种情况,并强制关闭应用。

错误示例:

// 错误的做法,会导致主线程阻塞
@Override
public void onPause() {
    super.onPause();
    try {
        receiveMsgThread.join(); // 或者 receiveMsgThread.wait();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

上述代码在 onPause() 方法中调用了 receiveMsgThread.join(),试图等待 receiveMsgThread 线程执行完毕。如果 receiveMsgThread 线程执行时间过长,就会导致 UI 线程阻塞,应用无响应。

正确的做法:使用后台线程和回调

解决这个问题的正确方法是将耗时操作放在后台线程中执行,并通过回调机制将结果返回给主线程。

  1. 创建后台线程: 使用 AsyncTask、HandlerThread 或 ExecutorService 等方式创建后台线程。

  2. 执行耗时操作: 在后台线程中执行耗时操作,例如网络请求、数据库查询等。

  3. 回调主线程: 使用 Handler 或 runOnUiThread() 方法将结果返回给主线程,以便更新 UI。

示例代码(使用 AsyncTask):

private class MyTask extends AsyncTask {

    @Override
    protected void onPreExecute() {
        // 在主线程中显示加载界面
        showLoadingDialog();
    }

    @Override
    protected String doInBackground(Void... params) {
        // 在后台线程中执行耗时操作
        try {
            Thread.sleep(3000); // 模拟耗时操作
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Operation completed!";
    }

    @Override
    protected void onPostExecute(String result) {
        // 在主线程中更新 UI
        dismissLoadingDialog();
        updateUI(result);
    }
}

// 启动 AsyncTask
new MyTask().execute();

在这个例子中,MyTask 继承自 AsyncTask,doInBackground() 方法在后台线程中执行耗时操作,onPreExecute() 和 onPostExecute() 方法在主线程中执行,分别用于显示加载界面和更新 UI。

使用 CountDownLatch 进行线程同步

如果需要更精细的线程同步控制,可以使用 CountDownLatch。CountDownLatch 允许一个或多个线程等待,直到计数器的值变为零。

import java.util.concurrent.CountDownLatch;

public class Worker implements Runnable {
    private final CountDownLatch doneSignal;
    private final int i;
    Worker(CountDownLatch doneSignal, int i) {
        this.doneSignal = doneSignal;
        this.i = i;
    }
    public void run() {
        try {
            doWork(i);
            doneSignal.countDown();
        } catch (InterruptedException ex) {} // return;
    }

    void doWork(int i) throws InterruptedException {
        System.out.println("Worker " + i + " is working...");
        Thread.sleep(1000); // Simulating work
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        int N = 5;
        CountDownLatch doneSignal = new CountDownLatch(N);
        for (int i = 0; i < N; ++i) // create and start threads
            new Thread(new Worker(doneSignal, i)).start();

        doneSignal.await();      // wait for all to complete
        System.out.println("All workers finished!");
    }
}

在这个例子中,CountDownLatch 初始化为 N,每个 Worker 线程完成任务后调用 countDown() 方法,计数器减 1。主线程调用 await() 方法等待,直到计数器变为 0,表示所有 Worker 线程都已完成。

总结

  • 永远不要在主线程中使用 wait() 或 join(),避免阻塞 UI 线程。
  • 使用后台线程处理耗时操作,并通过回调机制将结果返回给主线程。
  • 可以使用 AsyncTask、HandlerThread 或 ExecutorService 等方式创建后台线程。
  • 可以使用 CountDownLatch 进行更精细的线程同步控制。
  • 在执行耗时操作时,可以使用加载界面或其他机制来通知用户操作状态,提升用户体验。

通过遵循这些最佳实践,可以编写出更加流畅、响应迅速的 Android 应用。