技术学习这档子事儿,在学习技术点时,要先鸟瞰全貌,再逐步由点慢慢展开。而不是一开始就深入到细节里。学习Java并发编程,也是如此。我们要从单一的知识点中跳出来,高屋建瓴的去看并发编程,鸟瞰其全貌。

1 - 鸟瞰全貌

并发编程可以抽象成三个核心问题:分工、同步、互斥

1.1 - 分工

在并发编程领域,分工是指:将一个大任务,拆分成多个小任务,每个小任务由不同的线程去执行。
分工其实是很重要的,直接决定了并发程序的性能。JUC包中的Executor、Fork/Join、Future本质上就是一种分工办法。除此之外,并发编程领域还总结了几套设计模式,基本上是符合分工思想的。比如生产者-消费者、Thread-Per-Message、Worker-Thread等都是用来指导你分工的。

1.2 - 同步

并发编程领域里的同步,只要是指线程之间的协作。用于控制不同线程间操作发生相对顺序的机制。

协作一般是和分工相关的。Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是分工方法,但同时也能解决线程协作的问题。例如,用 Future 可以发起一个异步调用,当主线程通过 get() 方法取结果时,主线程就会等待,当异步执行的结果返回时,get() 方法就自动返回了。主线程和异步线程之间的协作,Future 工具类已经帮我们解决了。除此之外,Java SDK 里提供的 CountDownLatch、CyclicBarrier、Phaser、Exchanger 也都是用来解决线程协作问题的。

工作中遇到的线程协作问题,基本上都可以描述为这样的一个问题:当某个条件不满足时,线程需要等待,当某个条件满足时,线程需要被唤醒执行。

例如,在生产者 - 消费者模型里,也有类似的描述,“当队列满时,生产者线程等待,当队列不满时,生产者线程需要被唤醒执行;当队列空时,消费者线程等待,当队列不空时,消费者线程需要被唤醒执行。”

在 Java 并发编程领域,解决协作问题的核心技术是管程,上面提到的所有线程协作技术底层都是利用管程解决的。管程是一种解决并发问题的通用模型,除了能解决线程协作问题,还能解决下面我们将要介绍的互斥问题。可以这么说,管程是解决并发问题的万能钥匙。所以说,这部分内容的学习,关键是理解管程模型,学好它就可以解决所有问题。其次是了解 Java SDK 并发包提供的几个线程协作的工具类的应用场景,用好它们可以妥妥地提高你的工作效率。

1.3 - 互斥

分工、同步主要强调的是性能,但并发程序里还有一部分是关于正确性的,用专业术语叫线程安全。

并发程序里,当多个线程同时访问同一个共享变量的时候,结果是不确定的。不确定,则意味着可能正确,也可能错误,事先是不知道的。而导致不确定的主要源头是可见性问题、有序性问题和原子性问题,为了解决这三个问题,Java 语言引入了内存模型,内存模型提供了一系列的规则,利用这些规则,我们可以避免可见性问题、有序性问题,但是还不足以完全解决线程安全问题。解决线程安全问题的核心方案还是互斥。

所谓互斥,指的是同一时刻,只允许一个线程访问共享变量。实现互斥的核心技术就是锁,Java 语言里synchronized、SDK 里的各种 Lock 都能解决互斥问题。虽说锁解决了安全性问题,但同时也带来了性能问题,那如何保证安全性的同时又尽量提高性能呢?可以分场景优化,Java SDK 里提供的 ReadWriteLock、StampedLock 就可以优化读多写少场景下锁的性能。还可以使用无锁的数据结构,例如 Java SDK 里提供的原子类都是基于无锁技术实现的。除此之外,还有一些其他的方案,原理是不共享变量或者变量只允许读。这方面,Java 提供了 Thread Local 和 final 关键字,还有一种 Copy-on-write 的模式。使用锁除了要注意性能问题外,还需要注意死锁问题。