通过前面的一些文章的讲解,相信大家都对并发编程的相关技术和知识有了一定的了解,⽐如 volatile、Atomic、synchronized 底层、读写锁、AQS、并发包下的集合类、线程池,等等。
但是相信大部分人还停留在理论阶段,真是把这些技术应用到开发场景中去实践的时候,会遇到大量的问题,我们需要对并发相关的技术有很深的理解,然后结合实际的业务场景来优化
接下来我们看看一些优秀的开源框架中的实际场景中遇到的问题,来分析其背后涉及到的并发的性能优化思想和实践
最典型的应用就是微服务注册中心服务的读写,我们都知道微服务注册中心,肯定得在内存中有一个服务注册表的概念,这个服务注册表里面就是存放各个微服务注册时发送来自己的地址信息,
那么问题来了,这个注册表的数据,肯定存在有线程读,也有线程写的情况,例如:服务启动时,肯定会将自己的服务实例信息注册到注册表里,别的服务也会来读去这个注册表的信息。如果对 同一份内存中的注册表数据不加任何读写锁,那么可能就会发生线程安全问题,导致数据错乱。
针对以上问题,我们自然想到对注册和读取接口加暴力加上synchronized来保证线程安全,虽然保证了数据安全,但这样显然是不合适的,这样做的话,相当于所有线程读写数据,全部串行,大大降低了性能
我们思考一下,我们想要的是什么效果?
其实就是一个线程在往注册表里写数据的时候,不让其他线程再去写数据了,我们在读数据的时候,其他人可以随便读,场景如下。
这样做有什么好处呢?其实⼤部分时候都是读操作,所以使⽤读锁可以让⼤量的线程同时来读数据,不需要阻塞不需要排队,保证⾼并发读的性能是⽐较⾼的。然后少量的时候是有服务上线要注册数据,写数据的场景是⽐较少的,此时写数据的时候,只能⼀个⼀个的加写锁然后写数据,同时写数据的时候就不允许别⼈来写数据了。
所以读写锁是非常适合这种读多写少的场景的。
nacos(springcloud ALibaba注册中心)读写实例用的就是一个读写并发集合类CopyOnWriteArrayList
经过上面的案例,我们采用读写锁来应对都多写少的并发问题,那么在高并发情况下,大量线程并发写内存共享数据,直接加写锁,必然导致所有线程都是串行化,必然会导致整体多并发性能和吞吐量大幅度降低。
要保证并发写数据的性能,我们可以引入内存缓冲分片机制,也就是将内存缓冲切分为多个分⽚,每个内存缓冲分⽚就对应⼀个锁。这样的话,可以根据自己的系统负载,调整内存缓冲分片数量,增加锁的的数量,相当于对每一块内存缓存区加分段锁
因为写内存的效率是很高的,整个流程就是获取锁,写内存,释放锁,大大提高了写数据的性能。示意图
当缓存区写满之后,是不是要考虑把缓冲区中的数据刷新到磁盘,只有一块缓冲区是不是同样会出现之前的并发读写问题,这时我们可以引入双缓冲区,当一块缓冲区写满之后,我们可以把这块缓冲区和已满缓冲区交换,把已满缓存区数据刷新到磁盘,空的继续负责接收数据(大家如果对kafka熟悉的话,kafak里写数据就用到了缓冲区)
按照上面思路,一个线程如果在持有锁,写数据的过程中,发现缓冲区已满,这时就会起刷新磁盘,刷新磁盘的过程中,此线程一直持有这把锁,如果在刷新磁盘时间过程中,会导致后续线程一直卡死,全部都在等待锁,严重可能会导致整体系统卡死
聪明的朋友们可能想到了,如果发现内存缓冲区 1 满了,然后就交换两个缓冲区。接着直接就释放锁,释放锁了之后再由这个线程将数据刷⼊磁盘中,刷磁盘的过程是不会占⽤锁的,然后后续的线程都可以继续获取锁,快速写⼊内存,接着释放锁。
总结
在平时工作场景中,遇到了并发问题,我们要根据实际情况,尽可能利用机器的性能,在cpu,内存,磁盘之间找到合适的操作方法
如果喜欢本内容的话,关注公众号查看更多内容,微信搜索:京城小人物,或者扫描下方二维码,动动小手给作者一个鼓励,比心!
上一篇