MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin Android多线程编程

2023-06-112.7k 阅读

Kotlin Android多线程编程基础概念

线程与进程

在深入Kotlin Android多线程编程之前,我们先来明确一下线程(Thread)和进程(Process)的概念。

进程是操作系统进行资源分配和调度的基本单位,每个进程都有独立的内存空间,包含代码、数据和其他资源。例如,在Android系统中,每一个运行的应用程序就是一个进程。

线程则是进程中的一个执行单元,它是CPU调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存空间等。多线程编程允许在一个进程内同时执行多个任务,充分利用CPU的多核特性,提高应用程序的响应速度和运行效率。

为什么要在Android中使用多线程

Android应用默认在主线程(也称为UI线程)中运行。主线程负责处理用户界面的绘制、事件处理等操作。如果在主线程中执行耗时操作,如网络请求、文件读取等,会导致主线程阻塞,从而使应用程序界面失去响应,出现ANR(Application Not Responding)错误。

为了避免这种情况,我们需要将耗时操作放在子线程中执行。这样,主线程可以继续处理UI相关的任务,保证应用程序的流畅性和响应性。

Kotlin中的线程创建与管理

继承Thread类

在Kotlin中,创建线程最基本的方式之一是继承Thread类。以下是一个简单的示例:

class MyThread : Thread() {
    override fun run() {
        // 这里编写线程执行的任务
        for (i in 1..5) {
            println("MyThread: $i")
        }
    }
}

fun main() {
    val myThread = MyThread()
    myThread.start()
    for (i in 1..5) {
        println("MainThread: $i")
    }
}

在上述代码中,我们创建了一个继承自Thread类的MyThread类,并在run方法中定义了线程要执行的任务。在main函数中,我们创建了MyThread的实例并调用start方法启动线程。同时,main函数本身也在主线程中执行,所以会看到主线程和MyThread线程交替输出。

实现Runnable接口

另一种常见的创建线程的方式是实现Runnable接口。这种方式更加灵活,因为一个类可以实现多个接口,但只能继承一个类。

class MyRunnable : Runnable {
    override fun run() {
        for (i in 1..5) {
            println("MyRunnable: $i")
        }
    }
}

fun main() {
    val myRunnable = MyRunnable()
    val thread = Thread(myRunnable)
    thread.start()
    for (i in 1..5) {
        println("MainThread: $i")
    }
}

这里我们定义了MyRunnable类实现Runnable接口,然后将MyRunnable的实例作为参数传递给Thread的构造函数来创建线程。同样,主线程和新创建的线程会交替执行。

使用Thread类的静态方法创建线程

Thread类还提供了一些静态方法来创建和启动线程。例如,Thread(Runnable target)构造函数和Thread.start()方法的组合可以简化线程的创建和启动过程。

fun main() {
    Thread {
        for (i in 1..5) {
            println("NewThread: $i")
        }
    }.start()
    for (i in 1..5) {
        println("MainThread: $i")
    }
}

在这个示例中,我们使用了Kotlin的lambda表达式来创建一个实现Runnable接口的实例,并直接调用start方法启动线程。

Android中的多线程编程框架

Handler机制

在Android开发中,Handler机制是一种用于在不同线程间进行通信的重要方式。它主要由HandlerMessageMessageQueueLooper组成。

  1. Handler:用于发送和处理消息。可以通过sendMessage(Message msg)等方法向MessageQueue发送消息,并在handleMessage(Message msg)方法中处理接收到的消息。

  2. Message:消息对象,用于携带数据,如whatarg1arg2等字段,还可以通过obj字段携带任意对象。

  3. MessageQueue:消息队列,用于存储Handler发送过来的消息,按照先进先出的顺序处理。

  4. Looper:负责从MessageQueue中取出消息,并将其分发给对应的Handler处理。每个线程最多只能有一个Looper

以下是一个简单的示例,展示如何在子线程中更新UI:

class MainActivity : AppCompatActivity() {
    private lateinit var textView: TextView
    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            if (msg.what == 1) {
                textView.text = "Updated from thread"
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        textView = findViewById(R.id.textView)

        Thread {
            // 模拟耗时操作
            Thread.sleep(2000)
            val message = Message.obtain()
            message.what = 1
            handler.sendMessage(message)
        }.start()
    }
}

在上述代码中,我们在MainActivity中创建了一个Handler,它绑定到主线程的Looper。在子线程中,我们模拟了一个耗时操作,然后通过Handler发送一个消息,在handleMessage方法中更新UI。

AsyncTask

AsyncTask是Android提供的一个轻量级异步任务类,它简化了在子线程中执行任务并在主线程中更新UI的过程。AsyncTask有三个泛型参数:

  • Params:执行任务时传入的参数类型。
  • Progress:任务执行过程中更新进度的类型。
  • Result:任务执行完成后的返回结果类型。

AsyncTask主要有以下几个方法:

  • onPreExecute():在任务执行前在主线程中调用,用于初始化操作,如显示进度条。
  • doInBackground(Params... params):在子线程中执行任务,这里进行耗时操作,如网络请求、文件读取等。
  • onProgressUpdate(Progress... values):在主线程中调用,用于更新任务执行进度,如更新进度条。
  • onPostExecute(Result result):在任务执行完成后在主线程中调用,用于处理任务执行结果,如更新UI。

以下是一个使用AsyncTask下载图片并显示的示例:

class DownloadImageTask : AsyncTask<String, Void, Bitmap>() {
    private lateinit var imageView: ImageView

    constructor(imageView: ImageView) {
        this.imageView = imageView
    }

    override fun doInBackground(vararg urls: String): Bitmap? {
        val url = URL(urls[0])
        return try {
            val `in` = url.openStream()
            BitmapFactory.decodeStream(`in`)
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }

    override fun onPostExecute(result: Bitmap?) {
        if (result != null) {
            imageView.setImageBitmap(result)
        }
    }
}

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val imageView = findViewById<ImageView>(R.id.imageView)
        DownloadImageTask(imageView).execute("https://example.com/image.jpg")
    }
}

在这个示例中,DownloadImageTask类继承自AsyncTask,在doInBackground方法中下载图片,在onPostExecute方法中将下载的图片设置到ImageView上。

线程池

线程池是一种管理和复用线程的机制,它可以避免频繁创建和销毁线程带来的性能开销,提高应用程序的性能和稳定性。在Android中,可以使用ExecutorServiceThreadPoolExecutor来创建和管理线程池。

  1. 创建线程池

    • FixedThreadPool:固定大小的线程池,线程池中的线程数量固定,适用于执行大量的、耗时较短的任务。
    val executorService: ExecutorService = Executors.newFixedThreadPool(3)
    
    • CachedThreadPool:可缓存的线程池,线程池的大小会根据任务数量自动调整,适用于执行大量的、耗时较短的任务,且任务执行时间不确定的情况。
    val executorService: ExecutorService = Executors.newCachedThreadPool()
    
    • SingleThreadExecutor:单线程的线程池,只有一个线程在执行任务,适用于需要顺序执行的任务。
    val executorService: ExecutorService = Executors.newSingleThreadExecutor()
    
    • ScheduledThreadPool:可定时执行任务的线程池,适用于需要定时执行任务的场景,如定时更新数据。
    val scheduledExecutorService: ScheduledExecutorService = Executors.newScheduledThreadPool(3)
    
  2. 使用线程池执行任务

    val runnable = Runnable {
        // 线程执行的任务
        for (i in 1..5) {
            println("Thread in pool: $i")
        }
    }
    executorService.submit(runnable)
    
  3. 关闭线程池

    executorService.shutdown()
    

以下是一个完整的示例,展示如何使用线程池执行多个任务:

class MainActivity : AppCompatActivity() {
    private val executorService: ExecutorService = Executors.newFixedThreadPool(3)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        for (i in 1..5) {
            val task = Task(i)
            executorService.submit(task)
        }

        executorService.shutdown()
    }

    private class Task(private val taskNumber: Int) : Runnable {
        override fun run() {
            println("Task $taskNumber is running")
            for (j in 1..5) {
                println("Task $taskNumber: $j")
            }
        }
    }
}

在这个示例中,我们创建了一个固定大小为3的线程池,并提交了5个任务。线程池会依次执行这些任务,利用线程的复用提高效率。

Kotlin协程在Android多线程编程中的应用

协程基础概念

协程(Coroutine)是一种轻量级的线程模型,它可以在Kotlin中更简洁、高效地处理异步任务。与传统的线程和异步回调相比,协程具有以下优点:

  • 简洁的代码结构:使用asyncawait等关键字,代码更接近同步代码的写法,易于理解和维护。
  • 轻量级:协程的创建和销毁开销比线程小得多,适用于大量异步任务的场景。
  • 非阻塞挂起:协程可以在执行异步操作时挂起,而不会阻塞主线程,提高了程序的响应性。

创建和启动协程

在Kotlin中,可以使用GlobalScope.launchCoroutineScope.launch来创建和启动协程。GlobalScope是一个全局的协程作用域,而CoroutineScope可以自定义协程的生命周期。

import kotlinx.coroutines.*

fun main() = runBlocking {
    GlobalScope.launch {
        // 协程执行的任务
        for (i in 1..5) {
            println("GlobalScope: $i")
        }
    }

    CoroutineScope(Dispatchers.Default).launch {
        for (i in 1..5) {
            println("CoroutineScope: $i")
        }
    }

    for (i in 1..5) {
        println("Main: $i")
    }
}

在上述代码中,GlobalScope.launch创建的协程会在后台运行,不受main函数的生命周期限制。CoroutineScope.launch创建的协程在Dispatchers.Default调度器下执行,Dispatchers.Default表示使用默认的线程池来执行协程。main函数本身也在一个协程作用域中,通过runBlocking来阻塞主线程,直到所有协程执行完毕。

协程的挂起函数

协程中的挂起函数是指可以暂停协程执行,等待某个异步操作完成后再恢复执行的函数。例如,delay函数就是一个挂起函数,它会暂停协程指定的时间。

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        for (i in 1..5) {
            println("Before delay: $i")
            delay(1000)
            println("After delay: $i")
        }
    }
    for (i in 1..5) {
        println("Main: $i")
    }
}

在这个示例中,delay(1000)会使协程暂停1000毫秒,在此期间主线程不会被阻塞,继续执行main函数中的循环。

异步任务与结果获取

使用async函数可以启动一个异步任务,并返回一个Deferred对象,通过await方法可以获取异步任务的结果。

import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferred: Deferred<Int> = async {
        // 模拟耗时操作
        delay(2000)
        42
    }
    println("Waiting for result...")
    val result = deferred.await()
    println("Result is: $result")
}

在上述代码中,async启动了一个异步任务,在任务中模拟了2秒的耗时操作,然后返回42。通过await方法等待任务完成并获取结果。

协程在Android中的应用场景

  1. 网络请求:使用协程可以更简洁地处理网络请求,如使用OkHttp结合协程进行异步网络请求。
import kotlinx.coroutines.*
import okhttp3.*
import java.io.IOException

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        CoroutineScope(Dispatchers.IO).launch {
            val client = OkHttpClient()
            val request = Request.Builder()
               .url("https://example.com/api")
               .build()
            try {
                val response = client.newCall(request).execute()
                val result = response.body?.string()
                withContext(Dispatchers.Main) {
                    // 更新UI
                    textView.text = result
                }
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }
}

在这个示例中,我们在Dispatchers.IO调度器下执行网络请求,获取响应结果后,通过withContext(Dispatchers.Main)切换到主线程更新UI。

  1. 数据库操作:在Android中使用Room数据库时,协程可以方便地处理数据库的异步操作。
import androidx.room.*
import kotlinx.coroutines.flow.Flow

@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAllUsers(): Flow<List<User>>

    @Insert
    suspend fun insertUser(user: User)

    @Delete
    suspend fun deleteUser(user: User)
}

在上述UserDao接口中,insertUserdeleteUser方法使用了suspend关键字,表明它们是挂起函数,可以在协程中调用。

多线程编程中的同步与并发控制

线程安全问题

在多线程编程中,当多个线程同时访问和修改共享资源时,可能会出现线程安全问题。例如,以下代码展示了一个简单的线程安全问题:

class Counter {
    var count = 0
}

fun main() {
    val counter = Counter()
    val threads = List(10) {
        Thread {
            for (i in 1..1000) {
                counter.count++
            }
        }
    }
    threads.forEach { it.start() }
    threads.forEach { it.join() }
    println("Final count: ${counter.count}")
}

在这个示例中,多个线程同时对counter.count进行自增操作。由于多个线程可能同时读取和修改count的值,导致最终的结果可能不是预期的10000(10 * 1000)。

同步机制

  1. synchronized关键字:在Kotlin中,可以使用synchronized关键字来同步代码块,确保同一时间只有一个线程可以执行该代码块。
class Counter {
    var count = 0
    fun increment() {
        synchronized(this) {
            count++
        }
    }
}

fun main() {
    val counter = Counter()
    val threads = List(10) {
        Thread {
            for (i in 1..1000) {
                counter.increment()
            }
        }
    }
    threads.forEach { it.start() }
    threads.forEach { it.join() }
    println("Final count: ${counter.count}")
}

在上述代码中,synchronized(this)count++操作包裹起来,使得每次只有一个线程可以执行这个自增操作,从而保证了线程安全。

  1. ReentrantLockReentrantLock是Java提供的一种更灵活的锁机制,与synchronized关键字相比,它提供了更多的功能,如可中断的锁获取、公平锁等。
import java.util.concurrent.locks.ReentrantLock

class Counter {
    private val lock = ReentrantLock()
    var count = 0
    fun increment() {
        lock.lock()
        try {
            count++
        } finally {
            lock.unlock()
        }
    }
}

fun main() {
    val counter = Counter()
    val threads = List(10) {
        Thread {
            for (i in 1..1000) {
                counter.increment()
            }
        }
    }
    threads.forEach { it.start() }
    threads.forEach { it.join() }
    println("Final count: ${counter.count}")
}

在这个示例中,ReentrantLocklock方法获取锁,unlock方法释放锁。通过try - finally块确保无论是否发生异常,锁都会被正确释放。

并发控制工具

  1. SemaphoreSemaphore是一种信号量,它可以控制同时访问某个资源的线程数量。例如,以下代码展示了如何使用Semaphore来限制同时访问某个资源的线程数量为3:
import java.util.concurrent.Semaphore

class Resource {
    private val semaphore = Semaphore(3)
    fun accessResource() {
        semaphore.acquire()
        try {
            // 访问资源的代码
            println("Thread ${Thread.currentThread().name} is accessing the resource")
            Thread.sleep(1000)
        } catch (e: InterruptedException) {
            e.printStackTrace()
        } finally {
            semaphore.release()
        }
    }
}

fun main() {
    val resource = Resource()
    val threads = List(10) {
        Thread {
            resource.accessResource()
        }
    }
    threads.forEach { it.start() }
    threads.forEach { it.join() }
}

在上述代码中,Semaphore(3)表示允许同时有3个线程访问资源。acquire方法获取信号量,如果没有可用的信号量,线程会阻塞等待。release方法释放信号量,允许其他线程获取。

  1. CountDownLatchCountDownLatch可以让一个或多个线程等待其他线程完成一组操作后再继续执行。例如,以下代码展示了如何使用CountDownLatch来等待所有线程完成计算后再进行汇总:
import java.util.concurrent.CountDownLatch

class Calculator {
    private val latch = CountDownLatch(10)
    private var sum = 0
    fun calculate() {
        val threads = List(10) {
            Thread {
                val result = it * 10
                sum += result
                latch.countDown()
            }
        }
        threads.forEach { it.start() }
        try {
            latch.await()
            println("Final sum: $sum")
        } catch (e: InterruptedException) {
            e.printStackTrace()
        }
    }
}

fun main() {
    val calculator = Calculator()
    calculator.calculate()
}

在这个示例中,CountDownLatch(10)表示需要等待10个线程完成操作。每个线程在完成计算后调用latch.countDown()减少计数,主线程通过latch.await()等待所有线程完成,然后输出汇总结果。

通过深入理解和应用上述多线程编程的知识,开发者可以在Kotlin Android应用中高效地处理各种异步任务,提高应用程序的性能、响应性和稳定性。无论是使用传统的线程、Android特定的框架,还是Kotlin协程,都需要根据具体的应用场景选择合适的方式,并注意线程安全和并发控制等问题。