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

不知道这些“进阶产品”的你活该面试当炮灰……

时间:2023-03-15 11:18:37 科技观察

今天要讲一个很硬核的技术知识,我来分析一下CopyOnWrite的思路是什么,以及它在Java并发包传递中的具体体现,包括在Kafka内核源码中如何使用这种思想来优化并发性能。在面试过程中,这个CopyOnWrite很有可能成为面试官一击秒杀应聘者的王牌,也极有可能成为应聘者拿下offer的绝招。是比较高深的知识。多读少写带来的问题?你可以想象在我们的内存中有一个ArrayList。默认情况下,这个ArrayList绝对不是线程安全的。如果多个线程并发读写这个ArrayList,可能会出问题。.好了,问题来了,我们应该如何让这个ArrayList线程安全呢?有一种很简单的方法可以对这个ArrayList的访问加入线程同步控制。比如ArrayList必须在Synchronized代码段访问,这样一个线程才能同时操作它,或者可以通过ReadWriteLock读写锁来控制。我们假设使用ReadWriteLock读写锁来控制对这个ArrayList的访问。这样就可以同时执行多个读请求从ArrayList中读取数据,但是读请求和写请求是互斥的,写请求和写请求也是互斥的。看一下,代码大概类似于下面这样:().lock();//写锁到ArrayList.writeLock().unlock();}想一想,上面的代码有什么问题?最大的问题在于写锁和读锁互斥的区别。假设写操作频率低,读操作频率高,是写少读多的场景。那么在偶尔进行写操作的时候,会不会加写锁,此时会不会有大量的读操作被阻塞而无法执行呢?这是读写锁可能遇到的最大问题。引入CopyOnWrite思想解决问题这个时候就需要引入CopyOnWrite思想来解决问题了。它的思路是不用加任何读写锁,所有的锁都帮我去掉。如果有锁,就会有问题。如果有锁,就会有互斥。卡住了,无法执行。那么它是如何保证多线程并发的安全的呢?很简单,顾名思义,使用“CopyOnWrite”方法,从英文翻译成中文,大概就是“写数据时用副本执行”。当你在读数据的时候,不加锁也没关系。大家同时阅读,互不影响。问题主要是在写的时候。既然写的时候不能加锁,那就得采取策略了。如果你的ArrayList底层是一个数组,用来存放你的列表数据,那么比如你要修改数组中的数据,就必须先复制一份数组。然后你就可以在这个数组的副本中写入你要修改的数据,但是这个过程中你实际上是在操作一个副本。在这种情况下,读操作能否同时正常进行呢?这个写操作对读操作没有影响!请看下图,一起来体验一下这个过程:关键问题来了,那么写线程现在修改了复制数组之后,读线程现在如何感知这个变化呢?重点来了,划重点!这里我们需要用到Volatile关键字。作者之前写过一篇文章,讲解了Volatile关键字的使用。核心是让写线程修改变量后,其他线程可以立即读取该变量引用的最新值。这是Volatile的核心功能。.所以写线程一旦修改完复制数组,就可以使用Volatile写,将复制数组赋值给Volatile修改的数组的引用变量。只要赋值给Volatile修饰的变量,立刻对读线程可见,所有人都能看到最新的数组。下面是JDK中CopyOnWriteArrayList的源码://这个数组是核心,因为它用volatile修饰//只要你把最重要的数组赋值给它,其他线程马上就能看到最重要的数组privatetransientvolatileObject[]array;publicbooleanadd(Ee){finalReentrantLocklock=this.lock;lock.lock();try{Object[]elements=getArray();intlen=elements.length;//复制一份数组Object[]newElements=数组。copyOf(elements,len+1);//修改复制数组,比如在其中添加一个元素newElements[len]=e;//然后将复制数组赋值给volatile修改的变量setArray(newElements);returntrue;}finally{lock.unlock();}}看看他是如何在写数据的时候复制一份数组副本,然后修改副本,然后通过给Volatile变量赋值来更新修改后的数组副本.对其他线程可见。那么大家就想,因为更新是通过副本进行的,如果多个线程同时更新怎么办?如果有多个副本会不会有什么问题?当然,多个线程不能同时更新。这时候只要看一下在上面的源码中,加入了Lock机制,即同一时间只能有一个线程更新。那么更新的时候,会不会对读操作有什么影响呢?绝对不是,因为读操作很简单就是读取数组,并且不涉及任何锁。而且只要他完成了对Volatile修饰的变量的更新赋值,读线程就可以马上看到修改后的数组,这是Volatile保证的:privateEget(Object[]a,intindex){//最简单的一对arraysReadreturn(E)a[index];}这样就彻底解决了我们之前说的读多写少的问题。如果读写锁互斥,会导致写锁阻塞大量读操作,影响并发性能。但是如果用CopyOnWriteArrayList,就是用空间换时间。更新时,基于副本进行更新,避免锁。那么,最好使用Volatile变量赋值,保证可见性。更新时,对阅读线程没有影响!CopyOnWrite的思想在KafkaApplication源码中在Kafka的内核源码中,有这样一个场景,当客户端向Kafka写入数据时,会先将消息写入客户端本地的内存缓冲区,然后形成aBatch在内存缓冲区中,然后再写入性发送到Kafka服务器,有助于提高吞吐量。话不多说,我们来看下图:此时Kafka的内存缓冲区使用的是什么数据结构?看源码:privatefinalConcurrentMap>batches=newCopyOnWriteMap>();这个数据结构是用来存储写入内存缓冲区的消息的核心数据结构。要理解这个数据结构,需要在Kafka内核源码中解释很多概念,这里就??不展开了。但是大家注意一点,他自己实现了一个CopyOnWriteMap,而这个CopyOnWriteMap使用了CopyOnWrite的思想。我们看一下这个CopyOnWriteMap的源码实现://TypicalvolatilemodifiedordinaryMapprivatevolatileMapmap;@OverridepublicsynchronizedVput(Kk,Vv){//更新时,先创建一个副本,更新副本,然后然后给volatile变量赋值WritebackMapcopy=newHashMap(this.map);Vprev=copy.put(k,v);this.map=Collections.unmodifiableMap(copy);returnprev;}@OverridepublicVget(Objectk){//读取时,直接读取volatile变量引用的map数据结构,无需加锁returnmap.get(k);}这里之所以使用Kafka的核心数据结构实现CopyOnWriteMap的思路是因为这个Map的Key-Value是的,所以更新的不是那么频繁。即TopicPartition-Deque的Key-Value对,更新频率很低。但是它的Get操作是一个高频的读请求,因为它会频繁的读取一个TopicPartition对应的一个Deque数据结构来进行这个队列的入队、出队等操作,所以对于这个Map来说,频率最高的就是它的Get操作。这时Kafka采用了CopyOnWrite的思想来实现这个Map,避免了更新Key-Value时阻塞高频读操作,达到了无锁的效果,同时优化了线程并发性能。相信看完本文后,您对CopyOnWrite的思想和适用场景,包括在JDK中的实现,以及在Kafka源码中的应用,都有了切身体会。如果你能在面试的时候把这个思想及其在JDK中的体现说清楚,并结合知名开源项目Kafka的底层源码进一步向面试官解释,面试官对你的印象肯定会大大加分。中华石山:十余年BAT架构经验,一线互联网公司技术总监。带领数百人团队开发过亿级大流量高并发系统。多年工作积累的研究手稿和经验总结,现整理成文,一一传授。微信公众号:石山的建筑笔记(ID:shishan100)。