多线程并发访问共享数据可能导致意外或错误行为。Kotlin 允许我们控制多个线程对任何类型共享资源的访问。解决方案就是线程同步(Thread Synchronization)。
重要术语与概念
在我们开始使用同步前,先了解一些必要的术语和概念:
1)线程同步 是一种机制,用于确保两个或多个并发线程不会同时执行一段称为临界区(Critical Section) 的代码。
2)临界区是访问共享资源的代码区域,这段代码不能被多个线程同时执行。共享资源可能是变量、文件、输入/输出端口、数据库等。
我们来看一个例子:有一个名为 Counter
的类,它有一个字段 count
:
kotlin
class Counter {
var count = 0
fun inc() {
count++
}
}
代码说明:
这个类有一个名为 count
的变量和一个 inc()
方法,该方法用于将 count
增加 1。
现在我们用两个线程对该字段同时进行递增操作,每个线程递增 10,000,000 次:
kotlin
import kotlin.concurrent.thread
fun main() {
val counterInstance = Counter()
val thread1 = thread {
for (i in 1..10_000_000) {
counterInstance.inc()
}
}
val thread2 = thread {
for (i in 1..10_000_000) {
counterInstance.inc()
}
}
thread1.join()
thread2.join()
println("The result of the threads' work: ${counterInstance.count}")
}
代码说明:
我们创建了一个 Counter
实例并启动两个线程,每个线程对 count
执行 1 千万次递增。程序执行完毕后,输出 count
的值。
理论上最终值应该是:20,000,000,但实际运行结果却可能是错误的,比如:
kotlin
The result of the threads' work: 18696438
这是因为:
-
某些线程看不到其他线程对共享数据的更改 (可见性问题);
-
某些线程可能看到的是非原子操作的中间值 (原子性问题)。
这正是我们在多线程共享数据时所遇到的典型问题,因此多个线程递增一个值就是一个临界区问题。
当然,这只是个简单示例,实际的临界区可能要复杂得多。
同步代码
我们可以用经典方法来保护这段代码,使其不被多个线程同时访问,即使用同步方法。
Kotlin 中有两种方式实现同步:
1. 使用注解 @Synchronized
同步函数:
kotlin
@Synchronized
fun myFunction() {
// 执行某些操作
}
说明:
注解是一种为代码附加元数据的方式。在这里,@Synchronized
表示此方法在同一时刻只能被一个线程调用。
2. 使用 synchronized()
函数创建同步块:
kotlin
fun myOtherFunction() {
synchronized(this) {
// 同步代码块
}
}
说明:
同步方法或同步块需要一个对象来加锁。同一时刻只有一个线程 能执行同步块或同步方法中的代码,其他线程将会被阻塞,直到锁释放。
示例:同步函数
函数可以通过注解 @Synchronized
来同步。
同一时刻,一个对象的同步方法只能被一个线程执行;而多个对象的同步方法则可以同时被不同线程执行。
kotlin
class SomeClass(val className: String) {
@Synchronized
fun doSomething() {
val threadName = Thread.currentThread().name
println("$threadName entered the method of $className")
println("$threadName leaves the method of $className")
}
}
说明:
doSomething()
是一个同步方法,打印出进入和离开该方法的线程信息。
我们再创建一个线程类,调用 doSomething()
方法:
kotlin
class MyThread(val classInstance: SomeClass) : Thread() {
override fun run() {
classInstance.doSomething()
}
}
然后启动多个线程:
kotlin
val instance1 = SomeClass("instance-1")
val instance2 = SomeClass("instance-2")
val first = MyThread(instance1)
val second = MyThread(instance1)
val third = MyThread(instance2)
first.start()
second.start()
third.start()
输出可能是:
kotlin
Thread-0 entered the method of instance-1
Thread-2 entered the method of instance-2
Thread-0 leaves the method of instance-1
Thread-1 entered the method of instance-1
Thread-2 leaves the method of instance-2
Thread-1 leaves the method of instance-1
说明:
没有任何线程在同一时刻执行 instance-1
的方法,说明 @Synchronized
成功起到了同步作用。你可以多次运行它来观察行为。
示例:同步块
有时,我们只希望同步方法的一部分,可以使用 synchronized()
。
kotlin
class SomeClass {
var value = 0
fun changeValue(newValue: Int) {
// 非同步代码
print("I'd like to change the value for $newValue")
synchronized(this) {
// 同步代码
value = newValue
}
print("The value has been changed successfully!")
}
}
说明:
changeValue()
方法中只有 value = newValue
被加锁。
同步块基于 this
实例加锁,不同实例之间互不影响。
同步块虽然和同步方法相似,但提供了更灵活的控制,你可以只同步必要的代码部分。
总结
多线程中最重要的机制之一------线程同步 。
该机制能确保代码在多个线程同时访问共享数据时,能正确安全地运行。
两种同步代码的方式:
-
使用
@Synchronized
注解同步整个函数; -
使用
synchronized()
函数同步代码块。