一位10年Java程序员总结进阶中的你懂多线程和jvm优化吗?

感谢朋友们的认可和指正。本文是有感而发,因为看过了太多坑人的博客和书籍,感慨自己走过的弯路,不希望其他初学者被网上互相抄袭的博客和东拼西凑的书籍浪费时间,想以一个相对宏观的视野来描述一个概念,力求通俗易懂,所以没有深入太多细节,简化了很多模型,给部分朋友造成了疑惑,说声抱歉。也没有配图,都是抽时间手机码字,打个分割线都费劲,图呢,其实网上都有。

记得我在另外一篇答案中提到,计算机程序(不仅仅各种语言的代码,一切能向计算机发出指令的序列都是程序,当然包括Java虚拟机)的努力方向:最大化利用计算机资源。多线程就是如此,一个CPU密集型的任务在跑,你让IO干等着,这不是浪费吗?所以,这时候你启动一个IO密集型的任务,资源利用率就提升了。当然,这是一种简化模型,实际上一个人任务的不同阶段,需要的计算机资源是不同的,如果你能合理安排多个任务的执行逻辑,资源利用率就会很大提升。

我们学习程序语言,一定不要被束缚到语言细节和规范上去,而要从计算机逻辑执行层面思考问题。因为细节和规范都是人为设定的,是大牛抽象计算机逻辑后的加工品,你囿于此,其实是在理解别人的思想,而不是理解计算机。我们常说的高层依赖于抽象而不依赖于底层,是一样的意思。说了这么多,想表达的就是,对技术问题,要有思考的深度,要寻根溯源,要高屋建瓴。

回到多线程。上面提到synchronized,必须多说几句,这对理解锁的本质至关重要。多线程和锁,首先请大家记住一个场景:多人上厕所。

多线程和锁,一个是线程,一个是对象。一个在私有的线程栈中,一个在共享的堆中。如何标识某个线程持有某个锁对象?如何如何标志某个对象被某个线程锁定?很显然,线程栈中开启一片区域“栈帧”存储对象锁记录,堆中对象有对象头(对象头主要保存了对象的类元数据,以及对象的运行时状态,其中就包括了锁线程和GC分代等信息。)可以标识被哪个线程锁定。实际上,虚拟机就是利用对象头和monitor(后面讲)来实现锁的。

回到多人上厕所,人比做线程,厕所比做共享对象,锁比做对象头,monitor比做钥匙。

synchronized锁的是一个对象,或者是类的某个实例,或者是类本身(即常量池的Class)。synchronized内部原理是通过对象内部的一个叫做监视器(monitor)来实现的。本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,这就是为什么synchronized效率低的原因。比如Hashtable(再次吐槽小写t,浑身难受)和用Collections.synchronizedMap装饰的HashMap,内部都使用了 synchronized,所以性能差,不是因为“它性能差”,而是因为“它使用的同步方式”性能差,那天人家底层重写了性能高了你怎么办?很多时候,点下鼠标进入源码看几眼就知道的东西,没必要死记硬背。

synchronized这种依赖于操作系统所实现的锁我们称之为“重量级锁”。JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。别被这些名词砸晕了,这些锁的名字很有误导性,其实是对获取锁的方式的优化,不是锁。

所谓锁的优化,主要方向是优化获取锁的方式和加锁(释放)的方式。我不想一一解释枯燥名词。还是用上厕所举例。重量级锁可以认为是,你去上厕所,得先去管理处(人或者机器)登记并拿到钥匙上厕所,这个过程可以认为存在一次“用户态”到“内核态”的切换。是非常重量级的。

这里我必须强调一下,你的目标是上厕所,不是加锁,加锁只是为了你更好的上厕所。线程也一样,目的是为了完成某项任务。加锁是不得以为之的。

假如一层楼就你一个人,一个厕所, 你觉得还有必要去登记吗?要什么自行车?直接上啊。这就是无锁状态;如果这层楼还有一个哥们,但他尿泡比较强悍,一天不上厕所。厕所门上有个显示器,能显示上次上厕所的是谁、期间有没有其他人上厕所,那你上的时候,只要看下显示器就知道:没别人上过,还是我,照片都没变,不用刷脸,此厕可直接上。这就是偏向锁,因为“偏向你”;假如这个哥们偶尔也上一次,这次你发现厕所有别人上过,因为显示器上有他照片,那你就得重新刷脸,好吧,那我再刷了上吧,大部分时候,里面都没这哥们,你可顺利上厕所,这叫轻量级锁;如果某天这哥们腹泻(我一同事吃湖南蒸菜有过一次),那你悲剧了,你每次上的时候,不仅显示器不是你,你想刷脸进入,发展里面还有人。没办法,只能去管理处登记等待了,变成了重量级锁。锁升级是不会降级的。这里,重量级锁涉及操作系统的处理,而偏向锁和轻量级锁涉及CAS,硬件可以搞定,效率更高。

上述锁状态转移和加锁(解锁不讲了)是由虚拟机(配合操作系统)完成的,我们不可见,既然是虚拟机控制,当然就有相关参数,如是否启用偏向锁,我忘了参数名字,但我知道肯定有这样的参数。如果面试我的面试官因为我不知道参数名字鄙视我,我能反怼死他。记个别人定的名字很自豪?

上面讲到重量级锁的时候,其实就是锁竞争很激烈的时候。比如早上高峰期,厕所坑位紧缺,排队的人很多,如果你一直等,等待的状态就叫“自旋”,当然你可以自旋十分钟左右后离开(虚拟机自旋也有参数控制),因为你觉得里面的哥们玩手机不知道啥时候结束,你有更重要的事情要干,还不如去外面登记等通知。显然,自旋的前提是你知道上一个哥们不会很久。多次之后,你会摸清这些人上厕所的时间后,你自旋起来就更有针对性了,这叫“适应性自旋”。

还有,锁消除,锁粗化,比如基本没人用的StringBuffer、Vector,你用在某个方法中,其实根本没必要加锁,或者说比如连续的append,没必要每次都加锁,虚拟机就会进行锁消除或者锁粗化处理。

上面讲了这么多,主体是线程和锁对象,核心是获取锁的方式和锁定的方式,还有,不加锁或者“伪加锁”是不是能搞定?再次强调一遍,线程生来是为了完成任务的,不是为了和锁纠缠的。

多线程竞争锁的时候,肯定涉及到线程的排队,新来的线程怎么处理,是去竞争锁还是直接排队?排队中的线程,那些有资格竞争锁?有资格的线程,那个拿到锁(只是拿到锁,还未执行共享区)?不管怎么实现,这些东西是必须要考虑的。你在synchronized没见到,是因为虚拟机帮你处理了,涉及的队列也是虚拟机在维护。重量级锁的时候,又涉及和操作系统信号的交互。当然,要是你不用和操作系统进行如Mutex Lock这样“重量级的”交互也能更好、更快、更好的处理同步,那你就是大牛了。

大牛当然是存在,比如李老头。下面会开始讲更加灵活的、细粒度、可定制的Lock锁。可以认为是把synchronized加锁的过程、锁定的方式等流程中细节拆分出来,用灵巧的实现方式实现线程同步。再后面会讲对象的wait、notify,线程的sleep,主体不一样,思考的角度不一样。今天先到这里。

最后呢我想给大家分享一下我是怎么爬出这个坑的。因为我这边收集到了很多的学习视频,并且也关注了几个不错的课堂,下面我就给大家分享一个我的学习计划:

很多问题其实答案很简单,但是背后的思考和逻辑不简单,要做到知其然还要知其所以然。如果想学习Java工程化、高性能及分布式、深入浅出。性能调优、Spring,MyBatis,Netty源码分析的朋友可以加我的Java进阶架构学习群:952124565,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。

猜你喜欢

转载自blog.csdn.net/weixin_42882439/article/details/84838294