虽然该模型稍显原始且难以驾驭、不够可靠还有点危险,但仍然是并发编程的首选项,也是其他并发模型的基石。
该模型其实是对底层硬件运行过程的形式化。这种形式化既是该模型的最大优点、也是其最大的缺点。
该模型非常简单直接,几乎所有编程语言都提供了对该模型的支持,且不对其使用方式加以限制。
互斥:使用锁来保证某一时间仅有一个线程可以访问数据。它会带来竟态条件和死锁。
乱序执行的来源:
从直觉上来说,编译器、JVM、硬件都不应该修改原有的代码逻辑,但是近几年的运行效率提升,尤其是共享内存架构的运行效率提升,均基于此类代码的优化。
Java 内存模型为这类优化提供了标准。
Java 内存模型定义了一个线程对内存的修改何时对另一个线程可见。基本原则是,如果读线程和写线程不进行同步,就不能保证可见性。
很容易得出一个结论:让多线程代码安全运行的方法只能是让所有的代码都同步。但是这么做有两个缺点:
ReentrantLock 提供了显式的加解锁方法,可以在代码的不同位置来实现加解锁逻辑,这是 synchronized 块无法做到的。
同时,ReentrantLock 提供的 lockInterruptibly 方法可以用于终止死锁线程。
ReentrantLock 还可以为获取锁的超时设置超时时间。
设想我们要在链表插入一个节点,一种做法是用锁保护整个链表,但链表加锁时其他使用者无法访问该链表。而交替锁可以做到仅锁住链表的一部分,允许不涉及被锁部分的其他线程继续自由的访问链表。同样可以由 ReentrantLock 实现。
并发编程经常需要等待某个事件发生。比如从队列删除元素前需要等待队列非空、向缓存添加数据前需要等待缓存拥有足够的空间。这时就需要条件变量 Condition。
一个条件变量需要与一把锁关联,线程在开始等待条件之前必须先获取这把锁。获取锁后,线程检查所有等待的条件是否为真。如果条件为真,线程将解锁并继续执行。
如果条件不为真,线程会调优 await 方法,它将原子的解锁并阻塞等待该条件。
当另一个线程调用了 signal 或 signalAll,意味着对应的条件可能已变为真,await 方法将原子的恢复运行并重新加锁。
比如 AtomicInteger。与锁相比,原子变量有很多好处。首先,我们不会忘记在正确的时候获取锁;其次,由于没有锁的参与,对原子变量的操作不会引发死锁;最后,原子变量是无锁(lock-free)非阻塞(non-blocking)算法的基础,这种算法可以不使用锁和阻塞来达到同步的目的。
无锁代码比起有锁代码更加复杂,JUC 中的类都尽量使用了无锁代码。
ReentrantLock 和 JUC.atomic 突破了使用内置锁的限制,可以利用它们做到:
比如,编写服务端应用时为每个连接请求创建一个线程,这样存在两个隐患:
可以使用线程池来对线程进行复用,JUC 提供了各种类型的线程池。
比如 CopyOnWriteArrayList,它使用了保护性复制策略。它并非在遍历链表前进行复制,而是在链表被修改时复制,已经投入使用的迭代器会使用当时的旧副本。
应用多线程的难点不在编程,而在于难以测试。而测试中的一个大问题是难以复现。
随着项目的迭代和时间的流式,复杂的多线程代码会变得难以维护。