许多应用程序依赖并发和异步机制。然而,编写这样的代码很容易出错。这就是我们需要并发集合(Concurrent Collections) 的原因。
经典集合与多线程
如你所知,多个线程可能会同时访问相同的数据,如果不使用某种同步机制,通常会导致各种问题。
当多个线程访问一个集合时,也会出现类似问题:
-
大多数经典集合,如
Array
、List
、Map
和Set
,都是非同步的,因此不具备线程安全性。 -
有一些集合类型,如
Hashtable
、Vector
或Stack
(将在后续主题中讨论),是完全同步的,因此是线程安全的,但它们的性能很低。 -
当一个线程正在遍历一个完全同步的标准集合,而另一个线程试图向其添加新元素时,可能会抛出一个运行时异常
ConcurrentModificationException
。
下面的程序展示了一个竞态条件(race condition) 的例子,当两个线程向同一个集合中添加元素时就会发生这种情况:
import kotlin.concurrent.thread
fun addNumbers(target: MutableList<Int>) {
for (i in 0 until 100_000) target += i
}
fun main() {
val numbers = mutableListOf<Int>()
val writer = thread(start = false, name = "Thread 1", block = {
addNumbers(numbers)
})
writer.start()
addNumbers(numbers) // 主线程也添加数字
writer.join() // 等待子线程完成
println(numbers.size) // 结果可能会变化
}
解释代码
预期的结果是 200000
(主线程和子线程各添加 100000
个元素),但实际运行时,每次结果可能都不同,部分元素会丢失。
我们运行程序三次,得到以下结果:
162527
140487
143736
在多线程环境下,不要在未加同步的情况下使用标准集合。不过,需要注意的是,显式同步也可能导致性能下降,以及在大型程序中出现难以发现的错误。
并发集合(Concurrent Collections)
为了避免上述手动同步带来的问题,Java 类库提供了一些专门为多线程设计的集合实现,这些集合是线程安全的。你可以在 java.util.concurrent
包中找到它们,其中包括:
-
ConcurrentMap
-
ConcurrentLinkedQueue
-
CopyOnWriteArrayList
这些并发集合大大简化了现代 Java(或 Kotlin)应用的开发。
与使用 @Synchronized
注解不同,并发集合使用更复杂的同步原语(如锁分段、原子变量等)和无锁算法(lock-free algorithm) 来保证线程安全,并同时保持较高的性能。
注意:如果你的程序并不需要多线程处理,那么还是建议使用经典集合,它们在单线程环境中性能更优。
总结
在 Kotlin 中,如果你需要开发多线程程序,可以使用并发集合。它们已经实现了同步机制,能够解决多线程操作中的同步问题。像 ConcurrentMap
、ConcurrentLinkedQueue
和 CopyOnWriteArrayList
等集合是实现并发程序的良好选择。