Skip to content

Latest commit

 

History

History
94 lines (55 loc) · 6.26 KB

7-4.md

File metadata and controls

94 lines (55 loc) · 6.26 KB

7-4 线程安全性

Bukkit 服务器在运行时拥有数量庞大的线程,多线程使得程序从串行变为并发,合理利用了 CPU 的管道机制和多核性能。

!> 误区警示
很多人认为,多线程只不过是 CPU 在多个线程之间切换,因此并不能提升性能,这种观点是错误的
1. 首先,一个 CPU 可能有很多内核,它们可以同时进行处理。
2. 其次,CPU 解释机器指令(32 位或 64 位)时采用管道机制,即当前指令还在执行时,就开始解释下一条乃至再下一条指令,也可以实现并行。
3. 最后,涉及到磁盘、网卡等设备的外部 IO 操作时,有些电脑使用 DMA 技术,CPU 就可以不必参与数据读取的全过程,这时候 CPU 如果闲着还不如把处理能力放在其它的线程上。
4. 最重要的,多线程使得多任务成为可能,这对于非阻塞的事件驱动系统(如 Bukkit)是相当重要的,如果不能进行多任务,就会出现类似于当一个玩家移动时,其它玩家移动不了的情况。
所以,无论多线程是否能够带来性能上的提升,多线程都是必要的。而且事实证明,合理的多线程确实提升了速度。

Bukkit 中实现多线程的方法很多,但最常用的是重写 BukkitRunnable 类并进行 runTaskrunTaskLaterrunTaskAsynchronouslyrunTaskLaterAsynchoronously 等等。这个你应该已经见到过很多很多很多次了。因此,我不准备再讲一遍如何实现多线程了。

我们今天要讨论的问题更重要:线程安全

什么是线程安全

以下内容摘自维基百科。

线程安全是程式设计中的术语,指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。

也就是说,多个线程使用同一个变量,而不会导致冲突。

说得形象一点,这就像几个人共用一个洗手间。

  • 线程安全:先到的人(线程)进去在里面把门关上,其它人(线程)需要等待
  • 线程不安全:洗手间没有门,所有人(线程)一哄而上,后果……

一般情况下,多个线程访问同一个数据是不安全的。几个线程一起写数据,就可能会出现未知的后果。

要使得线程安全,我们就需要考虑不安全的条件:

  • 多个线程共享一个数据变量
  • 这些线程同时对数据变量进行操作

那我们只需要破坏这两个条件之一就可以了。事实上在 Java 的世界中,已经有了这样的方法。

  • 破坏第一个条件:为每个线程分配单独的数据(各用各的,谁也别抢, HarmonyAuth SMART 中 IDataManager 每次重新创建就是用的这种方法)
  • 破坏第二个条件:同步锁(一次一个,其它等着,HarmonyAuth SMART 中 sticli 方法前面的 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 得到保护,这三个同步方法(getsetadd)保证一次只能有一个线程读写 someList

上面的方法很麻烦,而且还容易忘掉这样写,幸运的是,有人已经为我们完成了这项工作,这就是第三种方法……

使用线程安全的实现

有些类在设计时就已经考虑到了多线程的情况,这些类通常可以处理多个线程同时访问(实际上是内部用了同步锁或者更高级的技巧)。

ArrayList 是我们常用的一个 List 的实现,但很遗憾,虽然它很快,但它不是线程安全的

ArrayList 相比,另一个 List 的实现 Vector 具有和 ArrayList 一样的功能,但它是线程安全的。这也就是说,「放心地让多个线程去读写它吧,没有问题的」。Vector 内部已经处理了多线程的情况。我们只需要 new,之后就可以在多个线程里面同时对它进行 setadd 等等操作了,不需要同步锁什么的,是不是很方便?

下面列出了一些常见的,非线程安全类的替代品:

非线程安全的类 线程安全的替代品
ArrayList(速度很快) Vector(速度较慢)
Stack(速度较慢)
HashMap(速度很快) ConcurrentHashMap(并发,速度略慢)
HashTable(速度较慢)
StringBuilder(速度较快) StringBuffer(速度较慢)

此外,CollectionssynchronizedCollection 方法可以「复制」一个线程安全的 Collection,注意,这是复制(创建一个新的),并不是就地修改。

List 也是一种 Collection,因此这个方法可以从已有的 List 对象创建一个数据不变的、新的、线程安全的对象。当然了,这样做速度会有所损失。


总之,线程安全是个麻烦事,在 new BukkitRunnable 之后一定要确认你的代码是不是线程安全的!如果不安全,最简单的方法就是把那个共享的变量改成诸如 Vector 这样的线程安全的实现