面试官扎心一问:知道 CopyOnWriteArrayList 吗?

news/2024/7/1 3:00:44

点击上方蓝色“方志朋”,选择“设为星标”

回复“666”获取独家整理的学习资料!

写入时复制(CopyOnWrite)思想

写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

CopyOnWriteArrayList的实现原理

在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。以下代码是向CopyOnWriteArrayList中add方法的实现(向CopyOnWriteArrayList里添加元素),可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。

/*** Appends the specified element to the end of this list.** @param e element to be appended to this list* @return <tt>true</tt> (as specified by {@link Collection#add})*/public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;Object[] newElements = Arrays.copyOf(elements, len + 1);newElements[len] = e;setArray(newElements);return true;} finally {lock.unlock();}}

读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。

public E get(int index) {return get(getArray(), index);
}

JDK中并没有提供CopyOnWriteMap,我们可以参考CopyOnWriteArrayList来实现一个,基本代码如下:

import java.util.Collection;
import java.util.Map;
import java.util.Set;public class CopyOnWriteMap<K, V> implements Map<K, V>, Cloneable {private volatile Map<K, V> internalMap;public CopyOnWriteMap() {internalMap = new HashMap<K, V>();}public V put(K key, V value) {synchronized (this) {Map<K, V> newMap = new HashMap<K, V>(internalMap);V val = newMap.put(key, value);internalMap = newMap;return val;}}public V get(Object key) {return internalMap.get(key);}public void putAll(Map<? extends K, ? extends V> newData) {synchronized (this) {Map<K, V> newMap = new HashMap<K, V>(internalMap);newMap.putAll(newData);internalMap = newMap;}}
}

实现很简单,只要了解了CopyOnWrite机制,我们可以实现各种CopyOnWrite容器,并且在不同的应用场景中使用。

几个要点

  • 实现了List接口

  • 内部持有一个ReentrantLock lock = new ReentrantLock();

  • 底层是用volatile transient声明的数组 array

  • 读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array

** volatile (挥发物、易变的)** :变量修饰符,只能用来修饰变量。volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变 化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

image
image

增删改查

1)增

public boolean add(E e) {final ReentrantLock lock = this.lock;//获得锁lock.lock();try {Object[] elements = getArray();int len = elements.length;//复制一个新的数组Object[] newElements = Arrays.copyOf(elements, len + 1);//插入新值newElements[len] = e;//将新的数组指向原来的引用setArray(newElements);return true;} finally {//释放锁lock.unlock();}
}public void add(int index, E element) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;if (index > len || index < 0)throw new IndexOutOfBoundsException("Index: "+index+", Size: "+len);Object[] newElements;int numMoved = len - index;if (numMoved == 0)newElements = Arrays.copyOf(elements, len + 1);else {newElements = new Object[len + 1];System.arraycopy(elements, 0, newElements, 0, index);System.arraycopy(elements, index, newElements, index + 1,numMoved);}newElements[index] = element;setArray(newElements);} finally {lock.unlock();}
}

2)删

public E remove(int index) {final ReentrantLock lock = this.lock;//获得锁lock.lock();try {Object[] elements = getArray();int len = elements.length;E oldValue = get(elements, index);int numMoved = len - index - 1;if (numMoved == 0)//如果删除的元素是最后一个,直接复制该元素前的所有元素到新的数组setArray(Arrays.copyOf(elements, len - 1));else {//创建新的数组Object[] newElements = new Object[len - 1];//将index+1至最后一个元素向前移动一格System.arraycopy(elements, 0, newElements, 0, index);System.arraycopy(elements, index + 1, newElements, index,numMoved);setArray(newElements);}return oldValue;} finally {lock.unlock();}
}

3)改

public E set(int index, E element) {final ReentrantLock lock = this.lock;//获得锁lock.lock();try {Object[] elements = getArray();E oldValue = get(elements, index);if (oldValue != element) {int len = elements.length;//创建新数组Object[] newElements = Arrays.copyOf(elements, len);//替换元素newElements[index] = element;//将新数组指向原来的引用setArray(newElements);} else {// Not quite a no-op; ensures volatile write semanticssetArray(elements);}return oldValue;} finally {//释放锁lock.unlock();}
}

4)查

public E get(int index) {return get(getArray(), index);}
private E get(Object[] a, int index) {return (E) a[index];}

CopyOnWrite的应用场景

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。实现代码如下:

import java.util.Map;import com.ifeve.book.forkjoin.CopyOnWriteMap;/*** 黑名单服务** @author fangtengfei**/
public class BlackListServiceImpl {private static CopyOnWriteMap<String, Boolean> blackListMap = new CopyOnWriteMap<String, Boolean>(1000);public static boolean isBlackList(String id) {return blackListMap.get(id) == null ? false : true;}public static void addBlackList(String id) {blackListMap.put(id, Boolean.TRUE);}/*** 批量添加黑名单** @param ids*/public static void addBlackList(Map<String,Boolean> ids) {blackListMap.putAll(ids);}}

代码很简单,但是使用CopyOnWriteMap需要注意两件事情:

1. 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。

2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。

CopyOnWrite的缺点

CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

「内存占用问题」。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

「针对内存占用问题」,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

「数据一致性问题」。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

CopyOnWriteArrayList为什么并发安全且性能比Vector好

我知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。

来源 | https://urlify.cn/mInAvq

热门内容:
  • 聊聊订单系统的设计?

  • 讨论:Service层需要接口吗?

  • 你还在认为 count(1) 比 count(*) 效率高?

  • 写了个牛逼的日志切面,甩锅更方便了!

最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。
明天见(。・ω・。)ノ♡

http://lihuaxi.xjx100.cn/news/269966.html

相关文章

java同步与死锁_Java多线程 - 线程同步与死锁

一、线程同步1)模拟多个用户同时从银行账户里面取钱● Account 类&#xff1a;银行账户类&#xff0c;里面有一些账户的基本信息&#xff0c;以及操作账户信息的方法//模拟银行账户classAccount {private String accountNo;//账号private double balance;//余额publicAccount()…

和我一起打造个简单搜索之SpringDataElasticSearch入门

网上大多通过 java 操作 es 使用的都是 TransportClient&#xff0c;而介绍使用 SpringDataElasticSearch 的文章相对比较少&#xff0c;笔者也是摸索了许久&#xff0c;接下来本文介绍 SpringDataElasticSearch 的 api 使用&#xff0c;更加方便的进行查询。 系列文章 一、和我…

精选一套火爆B站的硬核资源,请笑纳!

最近经常有粉丝给我留言求人工智能方向的资源&#xff0c;想要尝试自学人工智能&#xff0c;将自己的一些想法和创意付诸现实&#xff0c;或是想通过自学进入AI领域。细聊之下&#xff0c;大部分人自觉苦于不知从何入手&#xff0c;找不到学习重点。或是止步于晦涩难懂的理论和…

汇总 | 深度学习中图像语义分割基准数据集详解

点击上方“小白学视觉”&#xff0c;选择加"星标"或“置顶”重磅干货&#xff0c;第一时间送达汇总图像语义分割那些质量最好的数据集与常用benchmark数据集前言图像语义分割是计算机视觉最经典的任务之一&#xff0c;早期的图像分割主要有以下几种实现方法。基于像素…

自从上线了 Prometheus 监控告警,真香!

点击上方蓝色“方志朋”&#xff0c;选择“设为星标”回复“666”获取独家整理的学习资料&#xff01;对很多人来说&#xff0c;未知、不确定、不在掌控的东西&#xff0c;会有潜意识的逃避。当我第一次接触 Prometheus 的时候也有类似的感觉。对初学者来说&#xff0c; Promet…

常用浏览器插件

modify headers &#xff1a;firefox的IP伪造插件 httpRequester&#xff1a;firefox的模拟http请求插件JSON-handle&#xff1a;chrome格式化json插件firebug&#xff1a;firefox查看http请求工具firepath&#xff1a;firefox中获取元素路径转载于:https://www.cnblogs.com/xx…

es dsl java api_ElasticSearch 系列 - RestFulAPI(DSL)

前言DSL全称 Domain Specific language&#xff0c;即特定领域专用语言1.全局操作1.1 查询集群健康情况GET /_cat/health?v ?v表示显示头信息集群的健康状态有红、黄、绿三个状态&#xff1a;绿 – 一切正常(集群功能齐全)黄 – 所有数据可用&#xff0c;但有些副本尚未分配(…

性能超越最新序列推荐模型,华为诺亚方舟提出记忆增强的图神经网络

作者 | Chen Ma, Liheng Ma等译者 | Rachel出品 | AI科技大本营&#xff08;ID&#xff1a;rgznai100&#xff09;用户-商品交互的时间顺序可以揭示出推荐系统中用户行为随时间演进的序列性特征。用户与之交互的商品可能受到用户曾经接触的商品的影响。但是&#xff0c;用户和商…