gitbook/Java并发编程实战/docs/83267.md

82 lines
10 KiB
Markdown
Raw Permalink Normal View History

2022-09-03 22:05:03 +08:00
# 学习攻略 | 如何才能学好并发编程?
并发编程并不是一门相对独立的学科,而是一个综合学科。并发编程相关的概念和技术看上非常零散,相关度也很低,总给你一种这样的感觉:我已经学习很多相关技术了,可还是搞不定并发编程。那如何才能学习好并发编程呢?
其实很简单,只要你能从两个方面突破一下就可以了。一个是“跳出来,看全景”,另一个是“钻进去,看本质”。
## 跳出来,看全景
我们先说“跳出来”。你应该也知道,学习最忌讳的就是“盲人摸象”,只看到局部,而没有看到全局。所以,你需要从一个个单一的知识和技术中“跳出来”,高屋建瓴地看并发编程。当然,这**首要之事就是你建立起一张全景图**。
不过,并发编程相关的知识和技术还真是错综复杂,时至今日也还没有一张普遍认可的全景图,也许这正是很多人在并发编程方面难以突破的原因吧。好在经过多年摸爬滚打,我自己已经“勾勒”出了一张全景图,不一定科学,但是在某种程度上我想它还是可以指导你学好并发编程的。
在我看来,并发编程领域可以抽象成**三个核心问题:分工、同步和互斥**。
### 1\. 分工
所谓分工,类似于现实中一个组织完成一个项目,项目经理要拆分任务,安排合适的成员去完成。
在并发编程领域,你就是项目经理,线程就是项目组成员。任务分解和分工对于项目成败非常关键,不过在并发领域里,分工更重要,它直接决定了并发程序的性能。在现实世界里,分工是很复杂的,著名数学家华罗庚曾用“烧水泡茶”的例子通俗地讲解了统筹方法(一种安排工作进程的数学方法),“烧水泡茶”这么简单的事情都这么多说道,更何况是并发编程里的工程问题呢。
既然分工很重要又很复杂那一定有前辈努力尝试解决过并且也一定有成果。的确在并发编程领域这方面的成果还是很丰硕的。Java SDK并发包里的Executor、Fork/Join、Future本质上都是一种分工方法。除此之外并发编程领域还总结了一些设计模式基本上都是和分工方法相关的例如生产者-消费者、Thread-Per-Message、Worker Thread模式等都是用来指导你如何分工的。
学习这部分内容,最佳的方式就是和现实世界做对比。例如生产者-消费者模式,可以类比一下餐馆里的大厨和服务员,大厨就是生产者,负责做菜,做完放到出菜口,而服务员就是消费者,把做好的菜给你端过来。不过,我们经常会发现,出菜口有时候一下子出了好几个菜,服务员是可以把这一批菜同时端给你的。其实这就是生产者-消费者模式的一个优点,生产者一个一个地生产数据,而消费者可以批处理,这样就提高了性能。
### 2\. 同步
分好工之后,就是具体执行了。在项目执行过程中,任务之间是有依赖的,一个任务结束后,依赖它的后续任务就可以开工了,后续工作怎么知道可以开工了呢?这个就是靠沟通协作了,这是一项很重要的工作。
在并发编程领域里的同步,主要指的就是线程间的协作,本质上和现实生活中的协作没区别,不过是**一个线程执行完了一个任务,如何通知执行后续任务的线程开工**而已。
协作一般是和分工相关的。Java SDK并发包里的Executor、Fork/Join、Future本质上都是分工方法但同时也能解决线程协作的问题。例如用Future可以发起一个异步调用当主线程通过get()方法取结果时主线程就会等待当异步执行的结果返回时get()方法就自动返回了。主线程和异步线程之间的协作Future工具类已经帮我们解决了。除此之外Java SDK里提供的CountDownLatch、CyclicBarrier、Phaser、Exchanger也都是用来解决线程协作问题的。
不过还有很多场景,是需要你自己来处理线程之间的协作的。
工作中遇到的线程协作问题,基本上都可以描述为这样的一个问题:**当某个条件不满足时,线程需要等待,当某个条件满足时,线程需要被唤醒执行**。例如,在生产者-消费者模型里,也有类似的描述,“当队列满时,生产者线程等待,当队列不满时,生产者线程需要被唤醒执行;当队列空时,消费者线程等待,当队列不空时,消费者线程需要被唤醒执行。”
在Java并发编程领域解决协作问题的核心技术是**管程**,上面提到的所有线程协作技术底层都是利用管程解决的。管程是一种解决并发问题的通用模型,除了能解决线程协作问题,还能解决下面我们将要介绍的互斥问题。可以这么说,**管程是解决并发问题的万能钥匙**。
所以说这部分内容的学习关键是理解管程模型学好它就可以解决所有问题。其次是了解Java SDK并发包提供的几个线程协作的工具类的应用场景用好它们可以妥妥地提高你的工作效率。
### 3\. 互斥
分工、同步主要强调的是性能,但并发程序里还有一部分是关于正确性的,用专业术语叫“**线程安全**”。并发程序里当多个线程同时访问同一个共享变量的时候结果是不确定的。不确定则意味着可能正确也可能错误事先是不知道的。而导致不确定的主要源头是可见性问题、有序性问题和原子性问题为了解决这三个问题Java语言引入了内存模型内存模型提供了一系列的规则利用这些规则我们可以避免可见性问题、有序性问题但是还不足以完全解决线程安全问题。解决线程安全问题的核心方案还是互斥。
**所谓互斥,指的是同一时刻,只允许一个线程访问共享变量。**
实现互斥的核心技术就是锁Java语言里synchronized、SDK里的各种Lock都能解决互斥问题。虽说锁解决了安全性问题但同时也带来了性能问题那如何保证安全性的同时又尽量提高性能呢可以分场景优化Java SDK里提供的ReadWriteLock、StampedLock就可以优化读多写少场景下锁的性能。还可以使用无锁的数据结构例如Java SDK里提供的原子类都是基于无锁技术实现的。
除此之外还有一些其他的方案原理是不共享变量或者变量只允许读。这方面Java提供了Thread Local和final关键字还有一种Copy-on-write的模式。
使用锁除了要注意性能问题外,还需要注意死锁问题。
这部分内容比较复杂往往还是跨领域的例如要理解可见性就需要了解一些CPU和缓存的知识要理解原子性就需要理解一些操作系统的知识很多无锁算法的实现往往也需要理解CPU缓存。这部分内容的学习需要博览群书在大脑里建立起CPU、内存、I/O执行的模拟器。这样遇到问题就能得心应手了。
跳出来,看全景,可以让你的知识成体系,所学知识也融汇贯通起来,由点成线,由线及面,画出自己的知识全景图。
![](https://static001.geekbang.org/resource/image/11/65/11e0c64618c04edba52619f41aaa3565.png)
并发编程全景图之思维导图
## 钻进去,看本质
但是光跳出来还不够,还需要下一步,就是在某个问题上钻进去,深入理解,找到本质。
就拿我个人来说,我已经烦透了去讲述或被讲述一堆概念和结论,而不分析这些概念和结论是怎么来的,以及它们是用来解决什么问题的。在大学里,这样的教材很流行,直接导致了芸芸学子成绩很高,但解决问题的能力很差。其实,知其然知其所以然,才算真的学明白了。
我属于理论派,**我认为工程上的解决方案,一定要有理论做基础**。所以在学习并发编程的过程中我都会探索它背后的理论是什么。比如当看到Java SDK里面的条件变量Condition的时候我会下意识地问“它是从哪儿来的是Java的特有概念还是一个通用的编程概念”当我知道它来自管程的时候我又会问“管程被提出的背景和解决的问题是什么”这样一路探索下来我发现Java语言里的并发技术基本都是有理论基础的并且这些理论在其他编程语言里也有类似的实现。所以我认为技术的本质是背后的理论模型。
## 总结
当初我学习Java并发编程的时候试图上来就看Java SDK的并发包但是很快就放弃了。原因是我觉得东西太多眼花缭乱的虽然借助网络上的技术文章感觉都看懂了但是很快就又忘了。实际应用的时候大脑也一片空白根本不知道从哪里下手有时候好不容易解决了个问题也不知道这个方案是不是合适的。
我知道根本原因是,我的并发知识还没有成体系。
我想要让自己的知识成体系一定要挖掘Java SDK并发包背后的设计理念。Java SDK并发包是并发大师Doug Lea设计的他一定不是随意设计的一定是深思熟虑的其背后是Doug Lea对并发问题的深刻认识。可惜这个设计的思想目前并没有相关的论文所以只能自己琢磨了。
分工、同步和互斥的全景图,是我对并发问题的个人总结,不一定正确,但是可以帮助我快速建立解决并发问题的思路,梳理并发编程的知识,加深认识。我将其分享给你,希望对你也有用。
对于某个具体的技术,我建议你探索它背后的理论本质,理论的应用面更宽,一项优秀的理论往往在多个语言中都有体现,在多个不同领域都有应用。所以探求理论本质,既能加深对技术本身的理解,也能拓展知识深度和广度,这是个一举多得的方法。这方面,希望我们一起探讨,共同进步。
欢迎在留言区跟我分享你的经历与想法。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。