并发编程之入门

前言

​ 并发编程并非一门相对独立的学科,而是一个综合学科。并发编程相关的概念和技术看上很是零散,相关度也很低,总给你一种这样的感受:我已经学习不少相关技术了,可仍是搞不定并发编程。那如何才能学习好并发编程呢?算法

其实很简单,只要你能从两个方面突破一下就能够了。一个是“跳出来,看全景”,另外一个是“钻进去,看本质”。编程

跳出来,看全景

​ 咱们先说“跳出来”。你应该也知道,学习最忌讳的就是“盲人摸象”,只看到局部,而没有看到全局。因此,你须要从一个个单一的知识和技术中“跳出来”,高屋建瓴地看并发编程。固然,这首要之事就是你创建起一张全景图。设计模式

​ 不过,并发编程相关的知识和技术还真是错综复杂,时至今日也尚未一张广泛承认的全景图,也许这正是不少人在并发编程方面难以突破的缘由吧。好在通过多年摸爬滚打,我本身已经“勾勒”出了一张全景图,不必定科学,可是在某种程度上我想它仍是能够指导你学好并发编程的。缓存

在我看来,并发编程领域能够抽象成三个核心问题:分工、同步和互斥。安全

分工

​ 所谓分工,相似于现实中一个组织完成一个项目,项目经理要拆分任务,安排合适的成员去完成。数据结构

​ 在并发编程领域,你就是项目经理,线程就是项目组成员。任务分解和分工对于项目成败很是关键,不过在并发领域里,分工更重要,它直接决定了并发程序的性能。在现实世界里,分工是很复杂的,著名数学家华罗庚曾用“烧水泡茶”的例子通俗地讲解了统筹方法(一种安排工做进程的数学方法),“烧水泡茶”这么简单的事情都这么多说道,更况且是并发编程里的工程问题呢。并发

​ 既然分工很重要又很复杂,那必定有前辈努力尝试解决过,而且也必定有成果。的确,在并发编程领域这方面的成果仍是很丰硕的。Java SDK 并发包里的 Executor、Fork/Join、Future 本质上都是一种分工方法。除此以外,并发编程领域还总结了一些设计模式,基本上都是和分工方法相关的,例如生产者 - 消费者、Thread-Per-Message、Worker Thread 模式等都是用来指导你如何分工的。异步

​ 学习这部份内容,最佳的方式就是和现实世界作对比。例如生产者 - 消费者模式,能够类比一下餐馆里的大厨和服务员,大厨就是生产者,负责作菜,作完放到出菜口,而服务员就是消费者,把作好的菜给你端过来。不过,咱们常常会发现,出菜口有时候一会儿出了好几个菜,服务员是能够把这一批菜同时端给你的。其实这就是生产者 - 消费者模式的一个优势,生产者一个一个地生产数据,而消费者能够批处理,这样就提升了性能。工具

同步

​ 分好工以后,就是具体执行了。在项目执行过程当中,任务之间是有依赖的,一个任务结束后,依赖它的后续任务就能够开工了,后续工做怎么知道能够开工了呢?这个就是靠沟通协做了,这是一项很重要的工做。性能

​ 在并发编程领域里的同步,主要指的就是线程间的协做,本质上和现实生活中的协做没区别,不过是一个线程执行完了一个任务,如何通知执行后续任务的线程开工而已。

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

​ 工做中遇到的线程协做问题,基本上均可以描述为这样的一个问题:当某个条件不知足时,线程须要等待,当某个条件知足时,线程须要被唤醒执行。例如,在生产者 - 消费者模型里,也有相似的描述,“当队列满时,生产者线程等待,当队列不满时,生产者线程须要被唤醒执行;当队列空时,消费者线程等待,当队列不空时,消费者线程须要被唤醒执行。”

​ 在 Java 并发编程领域,解决协做问题的核心技术是管程,上面提到的全部线程协做技术底层都是利用管程解决的。管程是一种解决并发问题的通用模型,除了能解决线程协做问题,还能解决下面咱们将要介绍的互斥问题。能够这么说,管程是解决并发问题的万能钥匙。

因此说,这部份内容的学习,关键是理解管程模型,学好它就能够解决全部问题。其次是了解 Java SDK 并发包提供的几个线程协做的工具类的应用场景,用好它们能够妥妥地提升你的工做效率。

互斥

​ 分工、同步主要强调的是性能,但并发程序里还有一部分是关于正确性的,用专业术语叫“线程安全”。并发程序里,当多个线程同时访问同一个共享变量的时候,结果是不肯定的。不肯定,则意味着可能正确,也可能错误,事先是不知道的。而致使不肯定的主要源头是可见性问题、有序性问题和原子性问题,为了解决这三个问题,Java 语言引入了内存模型,内存模型提供了一系列的规则,利用这些规则,咱们能够避免可见性问题、有序性问题,可是还不足以彻底解决线程安全问题。解决线程安全问题的核心方案仍是互斥。

​ 所谓互斥,指的是同一时刻,只容许一个线程访问共享变量。

​ 实现互斥的核心技术就是锁,Java 语言里 synchronized、SDK 里的各类 Lock 都能解决互斥问题。虽然说锁解决了安全性问题,但同时也带来了性能问题,那如何保证安全性的同时又尽可能提升性能呢?能够分场景优化,Java SDK 里提供的 ReadWriteLock、StampedLock 就能够优化读多写少场景下锁的性能。还可使用无锁的数据结构,例如 Java SDK 里提供的原子类都是基于无锁技术实现的。

​ 除此以外,还有一些其余的方案,原理是不共享变量或者变量只容许读。这方面,Java 提供了 Thread Local 和 final 关键字,还有一种 Copy-on-write 的模式。

​ 使用锁除了要注意性能问题外,还须要注意死锁问题。

​ 这部份内容比较复杂,每每仍是跨领域的,例如要理解可见性,就须要了解一些 CPU 和缓存的知识;要理解原子性,就须要理解一些操做系统的知识;不少无锁算法的实现每每也须要理解 CPU 缓存。这部份内容的学习,须要博览群书,在大脑里创建起 CPU、内存、I/O 执行的模拟器。这样遇到问题就能驾轻就熟了。

跳出来,看全景,可让你的知识成体系,所学知识也融汇贯通起来,由点成线,由线及面,画出本身的知识全景图。

1552226930428

相关文章
相关标签/搜索