多线程与并发编程是 Android 开发中的重要内容,掌握它们能够帮助你更高效地处理后台任务,提升应用的响应速度和流畅度。
1. 基本概念
线程与进程:理解线程和进程的区别,线程是程序执行的基本单位,一个进程可以包含多个线程。
并发与并行:并发指的是多个任务在同一时间段内交替进行,可能是单核 CPU 上的时间片轮转;并行则是多个任务在多核 CPU 上同时执行。
死锁、竞态条件与活锁:理解并发编程中的常见问题,学会如何避免它们。
- 死锁:多个线程互相等待对方释放资源,导致程序无法继续执行。
- 竞态条件:多个线程并发访问共享资源时,结果依赖于执行顺序,导致程序行为不确定。
- 活锁:线程不断地互相调整状态,但永远无法完成任务。
2. Java 中的线程基础
Thread 类与 Runnable 接口:
Thread
类是 Java 中创建线程的传统方式,你可以继承Thread
类并重写run()
方法。Runnable
接口是一种更灵活的方式,线程通过实现Runnable
接口的run()
方法来执行任务。
线程启动方式:
// 使用 Thread 类(第一种方式更加简洁,且两者的效果相同)
Thread thread = new Thread(new MyRunnable());
thread.start();
// 使用 Runnable 接口
Runnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
3. 线程池(Executor Framework)
Executor:Java 提供了 Executor
接口和 ExecutorService
接口来简化线程管理,避免直接管理线程池。常用的实现类包括 ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
。
线程池的创建:
Executors.newFixedThreadPool(int nThreads)
:创建一个固定大小的线程池。Executors.newCachedThreadPool()
:创建一个可根据需要创建新线程的线程池。Executors.newSingleThreadExecutor()
:创建一个单线程池。
线程池的使用:
ExecutorService executorService = Executors.newFixedThreadPool(4);
executorService.submit(new MyRunnable()); // 提交任务到线程池
executorService.shutdown(); // 关闭线程池
4. 同步与互斥
synchronized 关键字:通过 synchronized
关键字来保证同一时刻只有一个线程可以执行同步代码块,避免竞态条件。
- 同步方法:
public synchronized void method() { }
- 同步代码块:
synchronized (object) { }
死锁:避免死锁的一种方法是按固定顺序请求资源,或使用 tryLock()
防止长时间等待锁。
ReentrantLock:ReentrantLock
是 java.util.concurrent
包中的一种锁,它比 synchronized
更加灵活,支持尝试锁定和定时锁定。
lock()
和unlock()
:手动获取和释放锁。tryLock()
:尝试获取锁,如果无法获得锁则立即返回。
5. 并发容器
Concurrent Collections:java.util.concurrent
包提供了一些线程安全的集合类,解决了多线程环境下常规集合类的并发问题。
- ConcurrentHashMap:线程安全的
Map
实现,支持并发读取和写入。 - CopyOnWriteArrayList:线程安全的
List
实现,适合在读取多、写入少的场景使用。 - BlockingQueue:线程安全的队列,提供了阻塞操作。
6. 高级同步机制
- CountDownLatch:一个同步辅助工具,用于阻塞一个或多个线程,直到其他线程执行完预定的操作。
- CyclicBarrier:一个同步工具,它允许一组线程互相等待,直到所有线程都达到某个共同屏障点。
- Semaphore:控制同时访问某个资源的线程数。
- Exchanger:两个线程通过此工具交换数据。
7. 并发工具类
ExecutorService:管理线程池并执行任务。
ScheduledExecutorService:执行定时任务,例如周期性任务。
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(“Scheduled task”);
}
}, 0, 1, TimeUnit.SECONDS); // 延迟 0 秒后每秒执行一次
ForkJoinPool:针对大规模数据处理的线程池,使用工作窃取算法,将任务分解成多个小任务并行处理。
8. Java 8 引入的并发功能
CompletableFuture:是 Future
的增强版,允许你更灵活地组合多个异步任务。
CompletableFuture.supplyAsync(() -> {
return “Task Result”;
}).thenAccept(result -> {
System.out.println(result);
});
Lambda 表达式与并发:Java 8 的 Lambda 表达式可以让线程池的任务定义变得更加简洁和易读。
9. Android 中的多线程编程
Android 中的 UI 操作必须在主线程(即 UI 线程)中执行,而耗时操作应在子线程中执行。常见的 Android 多线程编程工具和技巧包括:
AsyncTask:虽然已经被弃用,但仍然在老项目中广泛使用,适用于简单的后台任务执行。
new AsyncTask<Void, Void, String>() {
@Override
protected String doInBackground(Void… params) {
// 执行后台任务
return “Result”;
}
@Override
protected void onPostExecute(String result) {
// 在主线程更新 UI
textView.setText(result);
}
}.execute();
Handler 和 Looper:通过 Handler
将任务从后台线程发送到主线程,更新 UI。
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// 在主线程中执行任务,例如更新 UI
}
});
10. Kotlin 协程
Kotlin 协程(Coroutines)提供了一个更简洁、易用的方式来处理异步任务。它通过挂起函数(suspend
)和轻量级线程来简化并发编程。
launch
和async
:这两个函数分别用于启动协程并执行任务。suspend
函数:用于挂起当前协程并在后台执行任务,不阻塞线程。withContext
:切换上下文,确保耗时任务在后台线程执行,更新 UI 时切回主线程。
GlobalScope.launch {
val result = async { longRunningTask() }
withContext(Dispatchers.Main) {
textView.text = result.await()
}
}
11. 调度与线程管理
- 线程池管理:通过合理配置线程池的大小,避免过多的线程创建和销毁带来的性能开销。
- 线程优先级:在多线程环境中调整线程的优先级,确保重要任务优先执行。
12. 性能优化
- 避免过度创建线程:过多的线程会引起上下文切换,降低性能。
- 减少锁的使用:过多的锁会导致线程阻塞,影响性能。使用读写锁、
ReentrantLock
等优化锁的使用。 - 内存泄漏和 GC:确保线程中的对象不会被无意间引用,导致内存泄漏。