并发编程的基本理解
本文来自《Java并发编程实战》的引言,主要介绍了应该如何学习并发编程,以及并发编程的总结归纳。
并发出现的主要目的是为了提高整个系统的性能。并发编程不是一门独立的学科,我们在学习的时候要做到两点,一个是“跳出来,看全景”,另一个是“钻进去,看本质”。
跳出来,看全景
由于并发编程相关的知识和技术很多,我们在学习的时候要建立一张全景图,总的来说,并发编程领域可以抽象成三个核心问题:分工、同步和互斥。
1、分工
分工指的是如何高效地拆解任务并分配给线程,它直接决定了并发程序的性能。Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是一种分工方法。并发编程领域还总结了一些设计模式,基本上都是和分工方法相关的,例如生产者 - 消费者、hread-Per-Message、Worker Thread 模式等都是用来指导你如何分工的。
2. 同步
同步指的是线程之间如何协作,在项目执行过程中,任务之间是有依赖的,一个任务结束后,依赖它的后续任务就可以开工了。在并发编程领域里的同步,主要指的就是线程间的协作,本质上和现实生活中的协作没区别,不过是一个线程执行完了一个任务,如何通知执行后续任务的线程工而已。
协作一般是和分工相关的。Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是分工方法,但同时也能解决线程协作的问题。例如,用 Future 可以发起一个异步调用,当主线程通过 get() 方法取结果时,主线程就会等待,当异步执行的结果返回时,get() 方法就自动返回了。主线程和异步线程之间的协作,Future 工具类已经帮我们解决了。除此之外,Java SDK 里提供的 CountDownLatch、CyclicBarrier、Phaser、Exchanger 也都是用来解决线程协作问题的。
工作中遇到的线程协作问题,基本上都可以描述为这样的一个问题:当某个条件不满足时,线程需要等待,当某个条件满足时,线程需要被唤醒执行。Java 并发编程领域,解决协作问题的核心技术是管程,上面提到的所有线程协作技术底层都是利用管程解决的。管程是一种解决并发问题的通用模型,除了能解决线程协作问题,还能解决下面我们将要介绍的互斥问题。可以这么说,管程是解决并发问题的万能钥匙。
3. 互斥
分工、同步主要强调的是性能,互斥强调的是安全(线程安全)。由于并发出现问题的主要原因是可见性问题、有序性问题和原子性问题,为了解决这三个问题,Java 语言引入了内存模型,但是还不足以完全解决线程安全问题。解决线程安全问题的核心方案还是互斥。
所谓互斥,指的是同一时刻,只允许一个线程访问共享变量。
实现互斥的核心技术就是锁,Java 语言里 synchronized、SDK 里的各种 Lock 都能解决互斥问题。虽说锁解决了安全性问题,但同时也带来了性能问题,那如何保证安全性的同时又尽量提高性能呢?可以分场景优化,Java SDK 里提供的 ReadWriteLock、StampedLock 就可以优化读多写少场景下锁的性能。还可以使用无锁的数据结构,例如 Java SDK 里提供的原子类都是基于无锁技术实现的。
除此之外,还有一些其他的方案,原理是不共享变量或者变量只允许读。这方面,Java 提供了 Thread Local 和 final 关键字,还有一种 Copy-on-write 的模式。
钻进去,看本质
但是光跳出来还不够,还需要下一步,就是在某个问题上钻进去,深入理解,找到本质。工程上的解决方案,一定要有理论做基础。比如,看到一些概念和结论时,去分析概念和结论的来源。一般的技术背后都有对应的理论模型。