安全性、活跃性以及性能问题
- 并发编程中我们需要注意的问题有很多,很庆幸前人已经帮我们总结过了,主要有三个方面,分别是:安全性问题、活跃性问题和性能问题。
安全性问题
- 那什么是线程安全呢?其实本质上就是正确性,而正确性的含义就是程序按照我们期望的执行,不要让我们感到意外。
- 存在共享数据并且该数据会发生变化,通俗地讲就是有多个线程会同时读写同一数据。
- 那如果能够做到不共享数据或者数据状态不发生变化,不就能够保证线程的安全性了。
- 有不少技术方案都是基于这个理论的,例如线程本地存储(Thread Local Storage,TLS)、不变模式等等。
- 当多个线程同时访问同一数据,并且至少有一个线程会写这个数据的时候,如果我们不采取防护措施,那么就会导致并发 Bug,对此还有一个专业的术语,叫做数据竞争(Data Race)。
- 所谓竞态条件,指的是程序的执行结果依赖线程执行的顺序。
- 那面对数据竞争和竞态条件问题,该如何保证线程的安全性呢?
- 其实这两类问题,都可以用互斥这个技术方案,而实现互斥的方案有很多,CPU 提供了相关的互斥指令,操作系统、编程语言也会提供相关的 API。
- 从逻辑上来看,我们可以统一归为:锁。
活跃性问题
- 所谓活跃性问题,指的是某个操作无法执行下去。
- 我们常见的“死锁”就是⼀种典型的活跃性问题,当然除了死锁外,还有两种情况,分别是“活锁”和“饥饿”。
- 有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况,这就是所谓的“活锁”。
- 解决“活锁”的方案很简单,尝试等待一个随机的时间就可以了。
- 所谓“饥饿”指的是线程因无法访问所需资源而无法执行下去的情况。
- 解决“饥饿”问题的方案很简单,有三种方案:一是保证资源充足,二是公平地分配资源,三就是避免持有锁的线程长时间执行。
- 在并发编程里,主要是使用公平锁。
- 所谓公平锁,是一种先来后到的方案,线程的等待是有顺序的,排在等待队列前面的线程会优先获得资源。
性能问题
- “锁”的过度使用可能导致串行化的范围过大,这样就不能够发挥多线程的优势了,而我们之所以使用多线程搞并发程
序,为的就是提升性能。 - Java SDK 并发包里之所以有那么多东西,有很大一部分原因就是要提升在某个特定领域的性能。
- 第一,既然使用锁会带来性能问题,那最好的方案自然就是使用无锁的算法和数据结构了。
- 在这这方有很多相关的技术,例如线程本地存储 (Thread Local Storage, TLS)、写入时复制 (Copy-on-write)、乐观锁等;
- Java 并发包里面的原子类也是一种无锁的数据结构;
- Disruptor 则是一个无锁的内存队列,性能都非常好。
- 第二,减少锁持有的时间。
- 互斥锁本质上是将并行的程序串行化,所以要增加并行度,⼀定要减少持有锁的时间。
- 这个方案具体的实现技术也有很多,例如使用细粒度的锁,一个典型的例子就是 Java 并发包里的 ConcurrentHashMap,它使用了所谓分段锁的技术;
- 还可以使用读写锁,也就是读是无锁的,只有写的时候才会互斥。
- 第一,既然使用锁会带来性能问题,那最好的方案自然就是使用无锁的算法和数据结构了。