Bukkit 服务器在运行时拥有数量庞大的线程,多线程使得程序从串行变为并发,合理利用了 CPU 的管道机制和多核性能。
!> 误区警示
很多人认为,多线程只不过是 CPU 在多个线程之间切换,因此并不能提升性能,这种观点是错误的。
1. 首先,一个 CPU 可能有很多内核,它们可以同时进行处理。
2. 其次,CPU 解释机器指令(32 位或 64 位)时采用管道机制,即当前指令还在执行时,就开始解释下一条乃至再下一条指令,也可以实现并行。
3. 最后,涉及到磁盘、网卡等设备的外部 IO 操作时,有些电脑使用 DMA 技术,CPU 就可以不必参与数据读取的全过程,这时候 CPU 如果闲着还不如把处理能力放在其它的线程上。
4. 最重要的,多线程使得多任务成为可能,这对于非阻塞的事件驱动系统(如 Bukkit)是相当重要的,如果不能进行多任务,就会出现类似于当一个玩家移动时,其它玩家移动不了的情况。
所以,无论多线程是否能够带来性能上的提升,多线程都是必要的。而且事实证明,合理的多线程确实提升了速度。
Bukkit 中实现多线程的方法很多,但最常用的是重写 BukkitRunnable
类并进行 runTask
、runTaskLater
、runTaskAsynchronously
、runTaskLaterAsynchoronously
等等。这个你应该已经见到过很多很多很多次了。因此,我不准备再讲一遍如何实现多线程了。
我们今天要讨论的问题更重要:线程安全。
以下内容摘自维基百科。
线程安全是程式设计中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
也就是说,多个线程使用同一个变量,而不会导致冲突。
说得形象一点,这就像几个人共用一个洗手间。
- 线程安全:先到的人(线程)进去在里面把门关上,其它人(线程)需要等待
- 线程不安全:洗手间没有门,所有人(线程)一哄而上,后果……
一般情况下,多个线程访问同一个数据是不安全的。几个线程一起写数据,就可能会出现未知的后果。
要使得线程安全,我们就需要考虑不安全的条件:
- 多个线程共享一个数据变量
- 这些线程同时对数据变量进行操作
那我们只需要破坏这两个条件之一就可以了。事实上在 Java 的世界中,已经有了这样的方法。
- 破坏第一个条件:为每个线程分配单独的数据(各用各的,谁也别抢, HarmonyAuth SMART 中
IDataManager
每次重新创建就是用的这种方法) - 破坏第二个条件:同步锁(一次一个,其它等着,HarmonyAuth SMART 中
sti
和cli
方法前面的synchronized
就是这样做的)
下面我们依次介绍这两种方法。
这个方法适用于那些并不真正需要共享的数据。例如 IDataManager
这样的工具对象,或者需要读取但不需要写入的对象。
在一个线程开始时(通常是 BukkitRunnable
中的 run
方法开头),对于可以单独分配的数据,应该立即创建或复制。
虽然分配单独的数据很简单,也不能做到数据共享,但它确实能够解决大部分不需要共享的数据的多线程访问的问题。
在一个方法前面加上 synchronized
就可以一次只允许一个线程调用该方法,将操作共享数据的方法放在里面就可以了。
例如:
private static List<String> someList = new ArrayList<>();
public synchronized String get(int index) {
return someList.get(index);
}
public synchronized void set(int index, String s) {
someList.set(index, s);
}
public synchronized void add(String s) {
someList.add(s);
}
这样 someList
得到保护,这三个同步方法(get
、set
和 add
)保证一次只能有一个线程读写 someList
。
上面的方法很麻烦,而且还容易忘掉这样写,幸运的是,有人已经为我们完成了这项工作,这就是第三种方法……
有些类在设计时就已经考虑到了多线程的情况,这些类通常可以处理多个线程同时访问(实际上是内部用了同步锁或者更高级的技巧)。
ArrayList
是我们常用的一个 List
的实现,但很遗憾,虽然它很快,但它不是线程安全的。
与 ArrayList
相比,另一个 List
的实现 Vector
具有和 ArrayList
一样的功能,但它是线程安全的。这也就是说,「放心地让多个线程去读写它吧,没有问题的」。Vector
内部已经处理了多线程的情况。我们只需要 new
,之后就可以在多个线程里面同时对它进行 set
、add
等等操作了,不需要同步锁什么的,是不是很方便?
下面列出了一些常见的,非线程安全类的替代品:
非线程安全的类 | 线程安全的替代品 |
---|---|
ArrayList (速度很快) |
Vector (速度较慢)Stack (速度较慢) |
HashMap (速度很快) |
ConcurrentHashMap (并发,速度略慢)HashTable (速度较慢) |
StringBuilder (速度较快) |
StringBuffer (速度较慢) |
此外,Collections
的 synchronizedCollection
方法可以「复制」一个线程安全的 Collection
,注意,这是复制(创建一个新的),并不是就地修改。
List
也是一种 Collection
,因此这个方法可以从已有的 List
对象创建一个数据不变的、新的、线程安全的对象。当然了,这样做速度会有所损失。
总之,线程安全是个麻烦事,在 new BukkitRunnable
之后一定要确认你的代码是不是线程安全的!如果不安全,最简单的方法就是把那个共享的变量改成诸如 Vector
这样的线程安全的实现。