在线观看样式会丢失,上传了 pdf 版本,zip 中有 ppt 及 keynote 原件。
Hello,大家好,很荣幸可以有这样的机会进行本次演讲,在这里感谢活动的组织者云+社区,同样感谢正在收看的各位观众。可能大部分观众都对我比较陌生,我先自我介绍一下,通常情况下我使用的笔名或者网络上的名称都是 KIWI。 KIWI 在英语中有很多很多的意思,比如新西兰人、奇异果、奇异鸟。而我用它的寓意正是奇异鸟,奇异鸟究竟奇异在什么地方呢?原来它跟其它鸟类最大的不同就是它没有翅膀,通常来说没有翅膀的鸟儿一般下场会很惨,但是 KIWI 没有,因为没有翅膀失去了天空的庇佑,但是通过不断的奔跑,使腿部极其强壮,一般小动物都不一定能打过它。所以我以 KIWI 这名字来勉励自己勤能补拙,努力,努力,再努力!
好了,铺垫完之后,我再自我介绍一下吧!
大家好,我是 KIWI ,一个在努力成长中的奇异鸟!
正如图片所示,本次分享的主要内容分三大块:
在第一节中,我们会把并发内容归个类,方便后续学习。
然后在第二节中,我们会一起探讨,为什么并发总是跟 bug 如影随形。
最后一节,我们会看看有哪些可以避免并发问题的方案。
话不多说,我们开始第一节的内容,在并发学习时,我们在学习什么?
不买关子,如图所示,其实我们可以简单把并发分成三个部分去学习,分工、同步、互斥,那它们又分别代表什么呢?
指的是如何高效地拆解任务并分配给线程,可以理解为线程层面的统筹方法。
统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关系复杂的科研项目的组织与管理中,都可以应用。
怎样应用呢?主要是把工序安排好。
比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶,茶杯要洗;火已生了,茶叶也有了。怎么办?
办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开了,泡茶喝。
办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了泡茶喝。
办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。
哪一种办法省时间?我们能一眼看出第一种办法好,后两种办法都窝了工。
这是小事,但这是引子,可以引出生产管理等方面的有用的方法来。《-- 统筹方法(数学家华罗庚著作文章)_百度百科。》
指的是线程之间如何协作,大部分情况下指的是线程之间的通信,比较典型是生产消费模型,生产者生产完后会自身会等待,然后通知消费者去消费。
指的是保证同一时刻只允许一个线程访问共享资源,比较典型的就是 java 里面的各种互斥锁。
为什么我们写并发代码时,经常出现莫明奇妙的问题?百思而不得其解?明明看起来一样的代码,结果却不一样?
答案一会再说,我们先从 CPU 说起
左图是一个经典的 IE 笑话,一提到 IE 可能大家立刻就有一个印象--慢,而在电脑部件里面也有一个 IE 般的存在。
过去的几十年间,电脑硬件一直在不断的飞速发展,但是一个根本矛盾一直没有得到改善,那就CPU、内存、IO 设备之间的速度差异。而且速度的差异还是天壤之别的差距,如果说 CPU 是火箭,那么内存就是小汽车,IO设备就马路边的共享单车,还是不带电的那种,这种速度差异带来的后果是什么呢?
后果就像中间的图片一样,导致最严重的后果就是 CPU 大部分时间是没事干的,IO 设备加载两小时,CPU 5 秒钟就处理完了。一方面造成 CPU 资源的浪费,另一方面造成程序的响应时间长到没法接收。
为了缓解这个问题,硬件工程师跟软件工程师都做出了很大的努力。
首先硬件工程师这边,我们CPU 跟 IO 设备的运行速度之间差异很大很大,所有中间有了一个内存缓冲这个差异,内存的速度介于CPU 与 IO 之间,我们会把一些常用的数据先加载到内存中,而不是在每次用的时候再去加载,这就很像我们用的 redis 之类的缓存,典型的空间换时间的设计。
这对于平衡两者的速度差距得到了很大的提升,而且还不只这一点,因为虽然内存要比 IO 设备快的多,但是比 CPU 还是差很多,在 CPU 看来,在座的都是垃圾,那要如果解决呢?跟内存一样。在 CPU 与 内存之间再加一个缓存,这个缓存是存在 CPU 里的,CPU 运行时会把常用的一些数据从内存中存到 CPU 缓存中,这样就不用每次再去内存中取了。这里硬件工程师就把能做的内存,做的差不多了,但是这里埋了一个坑,后面会说。
硬件工程师圆满完成工作后,然后到我们软件工程师这边,解决思路很清晰,CPU 这么厉害,一个可以打 10 个,那我们就让他同时打 10 个,能者多劳,解决方案就是并发编程,CPU 是直接处理线程,创建多个线程,CPU 处理完一个线程就接着处理下一个线程,而不是在一个线程上等死,这种方案也极大的提高了 CPU 的利用率。
到这里通过软硬件工程师联手,可以说已经很完美的解决了这个问题,以前要么在听歌,要么在打代码,但是有了多线程后,你现在可以一边听歌一边打代码,但是真的是完美解决了吗?
好了,说完 CPU 缓存,我们接着看多核 CPU。为啥会出现多核 CPU 呢,他们为什么不在一个核里接着提升性能呢?
原因很简单,技术上很难。
有多难?上图半跪的人正是英特尔时任总裁贝瑞特。
跪的原因是什么呢?因为承诺的新 CPU 一直没有发布,技术上达到了瓶颈,摩尔定律开始终结!
但是呢?事情突然有了变化,某天,一个脑路清奇的工程师,想到,我既然没办法,让一个 CPU 的性能提升两倍,但是我可以把原来一个 CPU 里面塞两个小 CPU,这不也是性能翻倍吗?从些 CPU 走向了多核时代,一去不返。
我们看完了 CPU,我们现在来看具体问题,上图的右侧,是复现代码。
运行完代码,你觉得结果是什么?
结果其实是程序卡死。
为什么?明明一个线程修改了 flag ,另一个线程应该运行完才对。
理论上是这样,在单核的机器上运行也的确是这样,但是在多核就会出现问题。
为什么呢?
可以看上图中的左边及中间的图片,问题就出在我们上面说的缓存上,两个核,缓存了两个 flag 变量,两个线程其实看到的不一样。
上图右边是问题复现代码,代码逻辑就是一个普通的不能再普通的单例代码。同很多文章上一样的代码。
那么它会出现问题吗?
答案是可能会,小概率会,原因如下:
注意:
new 操作的步骤(理论上)
还有最经典的问题,原子性问题,复现代码在右侧
原因就很简单了,因为线程切换导致的
并发问题一般都很诡异并且很复杂,很多人被搞的身心俱疲,然后领悟到了一条解决并发问题的终极之道:『并发编程的第一原则,那就是不要写并发程序』,有点像 36 计里面最厉害的一计,这不是笑话,其实系统中也有这种体现,所有微服务系统都有分布式事务问题,但是解决分布式事务问题,也有他的代价,使代码更复杂,性能也不好。所以我们一直做的就是尽可能绕开他,最好的解决分布式事务的方法就是不使用分布式事务,并发同样。
如果不得不用,出现问题可以按下列方法去解决上述提到的 3 个问题,第一个就 Java 内存模型。Java 内存模型的本质就是 规范了 JVM 如何提供按需禁用缓存和编译优化的方法。
我们上面第一个例子及第二个例子,可见性问题及有序性问题,原因就是因为缓存及编译优化造成的,那解决的思路就很简单,把缓存跟编译优化按需关闭就好了。而且 Java 内存模型就是干这个事的。当然,你不用去实现内存模型,那是写 JVM 的人作的,你只需要按它给的规范去写代码即可。
对于第三个原子性问题,我们只需要用一把锁,把要并发代码给保护起来即可,可以使用如上图所示 Java 原生提供的种类繁多的锁
因为时间原因,本次的分享到此结束了,感谢大家的耐心观看。当然,个人能力有限,分享过程中肯定有所疏漏,欢迎大家指正,再见。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。