当前位置: 首页 > 科技观察

Java并发编程1个月吐血最全的100道面试题总结

时间:2023-03-13 20:31:40 科技观察

内容1.大部分人还停留在Java并发的理论阶段2.中间件系统的内核机制:双缓冲机制3.百万并发技术挑战4.写内存数据的锁机制和序列化问题5.分片机制+分段锁机制6.缓冲区满时双缓冲区交换7.等等!刷盘不会导致锁持有时间过长吗?8.内存+磁盘并行写入机制9.为什么要使用双缓冲机制?10.总结一下这篇文章,说一下百万级并发中间件系统内核代码中的锁性能优化。很多同学对Java并发编程很感兴趣,学习了很多相关的技术和知识。比如volatile、Atomic、synchronized底层、读写锁、AQS、并发包下的集合类、线程池等等。1、Java并发还处于理论阶段。很多同学可能看过很多书,通过很多视频课程学习过。但是大部分人可能还停留在理论的底层,主要是理解理论,基本很少实践和使用并发相关的技术,也很少做复杂的中间件系统。事实上,当这些技术实际应用到中间件系统的开发中时,会遇到很多问题,需要对底层并发相关的技术有深刻的理解和掌握。然后再结合自己的实际业务场景,进行相应的技术优化和机制优化,以达到最佳效果。因此,本文将从笔者曾经带过的一个高并发中间件项目的内核机制出发,看一个实际场景中遇到的并发相关问题。同时,我们也会通过相应的伪代码演化一步步分析其背后的并发性能优化思路和实践,最后看看优化后的效果。2.中间件系统的核心机制:双缓冲机制这个中间件项目由于涉及核心项目问题,就不做整体阐述了。我们就拿其中涉及到的内核机制和对应的场景来给大家讲解一下。其实这个例子是大量开源中间件系统和大数据系统中涉及到的一个场景,即:将核心数据写入磁盘文件。比如大数据领域的hadoop、hbase、elasitcsearch,Java中间件领域的redis、mq,都涉及到核心数据写入磁盘文件的问题。而很多大型互联网公司开发的中年系统也会有这种场景。只是不同的中间件系统具有不同的功能和目标,所以在将核心数据写入磁盘文件的机制设计上存在一些差异。所以我们公司开发的这个中间件项目,简单来说,需要实现一个效果:开辟两个内存空间,也就是经典的内存双缓冲机制。然后核心数据进来,写入所有第一个缓冲区。写满后,一个线程执行将该缓冲区的数据批量刷写到磁盘文件的工作,其他线程可以同时继续写入另一个缓冲区。我们要达到的就是这样的效果。这样,当一个缓冲区正在刷新磁盘时,另一个缓冲区可以毫无延迟地接受来自其他线程的写入。核心数据的写入不会中断,可以源源不断的写入这个中间件系统。让我们看看下面的图片来了解场景。如上图所示,首先有很多线程需要写入buffer1,然后buffer1写满后,满线程会将buffer1中的数据flush到磁盘文件中,其他线程继续写buffer2。这样数据批量刷盘和连续写入内存buffer就不会耽误这两件事了。这是中间件系统设计中非常常用的一种机制。请看下图。3、百万并发的技术挑战首先给大家介绍一下这个中间件系统的背景:这是一个服务于特殊场景的中间件系统,整体是集群部署。然后,每个实例都部署了一台高配置的机器。定位是单机最高可以承载万级甚至十万级并发,整体集群可以支持百万级并发。因此对单机的写入性能和吞吐量要求极高。在超高并发的需求下,上图内核机制的设计显得尤为重要。如果做得不好,很容易导致写并发性能不佳,达不到上述要求。另外,这里还要提一点,类似的机制在很多其他系统中都有涉及。比如上一篇文章:《降级机制设计不当,线上系统瞬间崩溃...》,里面提到的一个系统也有类似的机制。唯一不同的是,那篇文章使用了这种机制作为MQ集群整体故障时的容灾和降级机制。和本文的高并发中间件系统有点不同,所以在设计上考虑的一些细节也有所不同。的。而且上一篇的题目是关于这个内存双缓冲机制的一个在线问题:瞬时超高并发下的系统卡死问题。4.内存数据写入的锁机制和序列化问题。首先,让我们考虑第一个问题。您的多个线程将同时写入同一个内存缓冲区。这一定有问题!因为并发写入内存共享数据时,必须加锁,否则必然会出现并发安全问题,导致内存数据混乱。所以这里,我们写了如下伪代码,首先考虑线程是如何写入内存缓冲区的。好了,这行代码完成后,对应下图,我们来看看。看到这里,你就遇到了Java并发的第一个性能问题。要知道在高并发场景下,大量线程会并发写入内存。如果直接这样加锁,势必会导致所有线程都被序列化。改变。即一个线程加锁,写数据,然后释放锁。然后下一个线程做同样的事情。这种序列化势必会导致系统整体并发性能和吞吐量的大幅下降。5、内存缓冲区分片机制+分段锁机制因此,需要在内存双缓冲机制中引入分段锁机制,即将内存缓冲区分成多个片段,每个内存缓冲区片段对应一把锁。这样你就可以根据你的系统压测结果调整内存片的数量,增加锁的数量,从而允许大量的线程并发写入内存。我们看下面的伪代码,实现这个块的内存缓冲区碎片机制:好!让我们看看到目前为止的画面是什么样子的:这里因为每个线程只是加锁,写内存,然后释放锁。因此,每个线程持有锁的时间都很短。单个内存片的并发写入已经过压力测试,达到每秒几百甚至上千是没有问题的。因此,我们在单机上开发的在线系统有几十到上百个内存缓冲区碎片。经过压力测试,这足以支持每秒数万的并发写入。如果使用机器资源的限制,也可以支持每秒100,000个并发写入。6.缓冲区满时双缓冲区交换那么当一个缓冲区满时,是否必须交换两个缓冲区?那么需要有一个线程将满缓冲区的数据刷新到磁盘文件中吗?这时候的伪代码,大家想一想,是不是如下:同样的,我们通过下图来看看这个机制的实现:7、等一下!刷盘不会导致锁持有时间过长吗?等一下同学们,如果按照上面的伪代码思路,肯定有一个问题:如果是线程,获取了锁,开始写内存数据。然后,发现内存满了,然后直接在持有锁的过程中,还执行了数据刷盘的操作,这是有问题的。要知道数据刷盘是很慢的。根据数据量的不同,可能需要几十毫秒甚至几百毫秒。这样的话,一个线程不就持有锁几十毫秒,甚至几百毫秒吗?当然,这是行不通的。以下线程正在等待获取锁,然后写入缓冲区2。你怎么能一直持有锁?一旦按照这种思路写代码,在高并发场景下,难免会出现线程持有锁数百毫秒的情况。当刷入数据到磁盘时,后续的数百个工作线程都卡在等待锁的环节,无能为力。严重的时候甚至会导致系统整体卡死。8、内存+磁盘并行写入机制所以此时正确的并发优化代码应该是发现内存缓冲区1满了,然后交换两个缓冲区。然后直接释放锁。锁释放后,线程会将数据刷新到磁盘。刷盘过程不会占用锁,后续线程可以继续获取锁,快速写入内存,然后释放锁。.下面看看伪代码的优化:根据上面伪代码的优化,此时刷盘和写内存是可以并行进行的。因为这里的核心点就是大大减少锁占用的时间,这是java并发锁优化的一个非常核心的思想。我们看下图,一起感受一下:9.为什么一定要使用双缓冲机制?其实看到这里,你可能或多或少地体会到了双缓冲机制的一些设计思想。如果只使用单个内存缓冲区,那么从中读取数据并刷入磁盘的过程也需要占用锁。这时,想要获取锁并写入内存缓冲区的线程无法获取到锁。因此,如果只使用单块缓冲区,势必会导致读取内存数据并刷到磁盘的过程,从而长期占用锁。导致大量线程卡在获取锁上,获取不到锁,进而无法将数据写入内存。这就是这里必须使用双缓冲机制的核心原因。十。总结最后,我们做一个总结。本文从作者团队开发的百万并发中间件系统的内核机制入手,带你了解Java并发锁定时:如何使用双缓冲机制和内存缓冲分片机制。分段锁机制磁盘+内存并行写入机制极大优化高并发场景下的多线程锁序列化竞争长期锁占用的问题在很多优秀的开源中间件系统中其实都存在。类似Java的并发优化机制,主要是为了大幅提升系统在高并发场景下的并发性能和吞吐量。如果有兴趣,也可以阅读相关的底层源码。

猜你喜欢