引言
并发编程并不是一门绝对独立的学科,而是一个综合学科。并发编程相干的概念和技术看上十分零散,相关度也很低,总给你一种这样的感觉:我曾经学习很多相干技术了,可还是搞不定并发编程。那如何能力学习好并发编程呢?
其实很简略,只有你能从两个方面冲破一下就能够了。一个是“跳进去,看全景”,另一个是“钻进去,看实质”。
跳进去,看全景
咱们先说“跳进去”。你应该也晓得,学习最禁忌的就是“盲人摸象”,只看到部分,而没有看到全局。所以,你须要从一个个繁多的常识和技术中“跳进去”,高屋建瓴地看并发编程。当然,这首要之事就是你建设起一张全景图。
不过,并发编程相干的常识和技术还真是盘根错节,时至今日也还没有一张广泛认可的全景图,兴许这正是很多人在并发编程方面难以冲破的起因吧。好在通过多年摸爬滚打,我本人曾经“勾画”出了一张全景图,不肯定迷信,然而在某种程度上我想它还是能够领导你学好并发编程的。
在我看来,并发编程畛域能够形象成三个外围问题:分工、同步和互斥。
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执行的模拟器。这样遇到问题就能得心应手了。
跳进去,看全景,能够让你的常识成体系,所学常识也融汇贯通起来,由点成线,由线及面,画出本人的常识全景图。并发编程全景图之思维导图如下:
钻进去,看实质
然而光跳进去还不够,还须要下一步,就是在某个问题上钻进去,深刻了解,找到实质。
就拿我集体来说,我曾经烦透了去讲述或被讲述一堆概念和论断,而不剖析这些概念和论断是怎么来的,以及它们是用来解决什么问题的。在大学里,这样的教材很风行,间接导致了芸芸学子问题很高,但解决问题的能力很差。其实,知其然知其所以然,才算真的学明确了。
我属于实践派,我认为工程上的解决方案,肯定要有实践做根底。所以在学习并发编程的过程中,我都会摸索它背地的实践是什么。比方,当看到Java SDK外面的条件变量Condition的时候,我会下意识地问,“它是从哪儿来的?是Java的特有概念,还是一个通用的编程概念?”当我晓得它来自管程的时候,我又会问,“管程被提出的背景和解决的问题是什么?”这样一路摸索下来,我发现Java语言里的并发技术根本都是有实践根底的,并且这些实践在其余编程语言里也有相似的实现。所以我认为,技术的实质是背地的实践模型。
小结
当初我学习Java并发编程的时候,试图上来就看Java SDK的并发包,然而很快就放弃了。起因是我感觉货色太多,目迷五色的,尽管借助网络上的技术文章,感觉都看懂了,然而很快就又忘了。理论利用的时候大脑也一片空白,基本不晓得从哪里下手,有时候好不容易解决了个问题,也不晓得这个计划是不是适合的。
我晓得根本原因是,我的并发常识还没有成体系。
我想,要让本人的常识成体系,肯定要开掘Java SDK并发包背地的设计理念。Java SDK并发包是并发巨匠Doug Lea设计的,他肯定不是随便设计的,肯定是三思而行的,其背地是Doug Lea对并发问题的粗浅意识。惋惜这个设计的思维目前并没有相干的论文,所以只能本人推敲了。
分工、同步和互斥的全景图,是我对并发问题的集体总结,不肯定正确,然而能够帮忙我疾速建设解决并发问题的思路,梳理并发编程的常识,加深意识。我将其分享给你,心愿对你也有用。
对于某个具体的技术,我倡议你摸索它背地的实践实质,实践的利用面更宽,一项优良的实践往往在多个语言中都有体现,在多个不同畛域都有利用。所以探究实践实质,既能加深对技术自身的了解,也能拓展常识深度和广度,这是个一举多得的办法。这方面,心愿咱们一起探讨,共同进步。