面试官:请手写一个 CopyOnWriteMap!
Posted 明明如月学长
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试官:请手写一个 CopyOnWriteMap!相关的知识,希望对你有一定的参考价值。
一、背景
Java 求职面试中, List
和 Map
可以说是经典的八股文必考知识点。
有些面试官喜欢问 ArrayList
和 HashMap
的相关知识,而有些面试官可能会独辟蹊径,重点考察其他 CopyOnWriteArrayList
。
或许,很多同学会说,这有什么难的呢?还不是信手拈来?
但,如果面试官让你现场手写一个 CopyOnWriteMap
你是否还能那么淡定从容?
本文将系统介绍下 CopyOnWriteArrayList
的原理、优缺点和使用场景,并根据给出 CopyOnWriteMap
的典型代码实现。
二、探讨
2.1 CopyOnWriteArrayList
的源码和原理
2.1.1 源码
下面是该类的一些关键代码:
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
private static final long serialVersionUID = 8673264195747942595L;
/**
* The lock protecting all mutators. (We have a mild preference
* for builtin monitors over ReentrantLock when either will do.)
*/
final transient Object lock = new Object();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray()
return array;
/**
* Sets the array.
*/
final void setArray(Object[] a)
array = a;
/**
* Creates an empty list.
*/
public CopyOnWriteArrayList()
setArray(new Object[0]);
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return @code true (as specified by @link Collection#add)
*/
public boolean add(E e)
synchronized (lock)
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices). Returns the element that was removed from the list.
*
* @throws IndexOutOfBoundsException @inheritDoc
*/
public E remove(int index)
synchronized (lock)
Object[] es = getArray();
int len = es.length;
E oldValue = elementAt(es, index);
int numMoved = len - index - 1;
Object[] newElements;
if (numMoved == 0)
newElements = Arrays.copyOf(es, len - 1);
else
newElements = new Object[len - 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index + 1, newElements, index,
numMoved);
setArray(newElements);
return oldValue;
/**
* Saves this list to a stream (that is, serializes it).
*
* @param s the stream
* @throws java.io.IOException if an I/O error occurs
* @serialData The length of the array backing the list is emitted
* (int), followed by all of its elements (each an Object)
* in the proper order.
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException
s.defaultWriteObject();
Object[] es = getArray();
// Write out array length
s.writeInt(es.length);
// Write out all elements in the proper order.
for (Object element : es)
s.writeObject(element);
/**
* Reconstitutes this list from a stream (that is, deserializes it).
* @param s the stream
* @throws ClassNotFoundException if the class of a serialized object
* could not be found
* @throws java.io.IOException if an I/O error occurs
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException
s.defaultReadObject();
// bind to new lock
resetLock();
// Read in array length and allocate array
int len = s.readInt();
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, len);
Object[] es = new Object[len];
// Read in all elements in the proper order.
for (int i = 0; i < len; i++)
es[i] = s.readObject();
setArray(es);
// 省略其他
从源码可以看出,CopyOnWriteArrayList
是一个线程安全的 ArrayList
,它的功能和 ArrayList
类似,但是它采用了一种读写分离的并发策略。它的底层同样是一个 Object
类型的数组,用来存放元素。当对 CopyOnWriteArrayList
进行写操作(增/删/改)时,它会使用 synchronized
关键字保证同时只有一个线程对数组进行修改,并且会创建数组的新副本来完成修改操作,然后将原数组引用指向新数组。这样就避免了写操作对读操作造成影响,提高了读操作的性能。
当然,面试官可能会问CopyOnWriteArrayList
中 volatile
关键字的作用。
CopyOnWriteArrayList
中 volatile
的作用是保证数组引用的可见性和有序性。
可见性是指当一个线程修改了volatile变量的值后,其他线程可以立即看到最新的值。
有序性是指禁止编译器和处理器对 volatile
变量的指令重排序。
也就是说,当写操作修改了数组引用时,其他线程可以及时看到最新的数组引用。同时,volatile
也防止了指令重排,保证了多线程写入的可见性。
volatile
的原理是基于 Java 内存模型(JMM)和 CPU 缓存一致性协议的。JMM 定义了主内存和工作内存的概念,以及它们之间的交互操作。CPU 缓存一致性协议是一种保证多个处理器之间共享变量的值一致的机制。volatile变量的读写操作会触发JMM的内存屏障(memory barrier),从而实现可见性和有序性的效果
但是,volatile 不能保证数组元素的可见性和有序性。也就是说,如果只修改了数组元素而没有修改数组引用,其他线程可能看不到最新的数组元素。这就可能导致一致性的问题。
面试官还有可能会问 CopyOnWriteArrayList
中 transient
关键字的作用
CopyOnWriteArrayList
中 transient
关键字的作用是在序列化中才起作用,transient
变量不会被自动序列化。也就是说,当 CopyOnWriteArrayList
对象被序列化时,它的数组元素不会被写入到输出流中。这样做的好处是节省了空间和时间,因为数组元素可能很多,而且可能已经被修改了。
如果要恢复CopyOnWriteArrayList
对象的状态,可以使用 writeObject
和 readObject
方法来自定义序列化和反序列化的逻辑。
2.1.2 CopyOnWriteArrayList
优缺点和适用场景
CopyOnWriteArrayList
主要优点:
- 线程安全,它可以在多线程的环境下保证数据的一致性。它对所有的写操作都使用ReentrantLock来加锁,避免了并发修改异常。
- 读性能高,它对所有的读操作都不加锁,大大提高了“读”操作的并发度。它使用了快照机制来实现迭代器,避免了迭代过程中的同步开销。
CopyOnWriteArrayList
主要缺点,比如:
- 内存占用大,因为写时复制的原理,所以在添加新元素的时候会复制一份,此刻内存中就会有两份对象,比如这个时候有200M,你在复制一份 400M,如果频繁出现这种情况,造成产生频繁的 JVM 的 Yong GC 和 Full GC,严重的会进行 STW。
- 存在数据一致性问题,因为写操作是在新数组上进行的,而读操作是在旧数组上进行的,所以可能会出现“脏读”的问题。也就是说,读操作可能访问到过期的数据。
- 不支持随机访问和迭代器修改,因为
CopyOnWriteArrayList
使用了快照机制来实现迭代器。也就是说,迭代器遍历的是数组的一个副本,并不反映数组的实时状态。如果试图用迭代器修改元素,会抛出UnsupportedOperationException
异常。 - 频繁并发写操作性能差。由于采用
synchronized
对写操作进行加锁,因此频繁多线程写容易因线程竞争锁而影响性能。
基于CopyOnWriteArrayList
原理和优缺点,我们可以看出它更适合于读多写少的并发场景。
2.2 CopyOnWriteMap
参考代码
对于很多面试官,如果你能将上述的原理、优缺点和适用场景对答如流可能就满意了。
然而,真正聪明的面试官更愿意考察你的知识迁移能力,通过让你手写 CopyOnWriteMap
来验证你是真正理解还是突击背诵。
JDK 没有直接给出 CopyOnWriteMap
的代码实现,本文给出 kafka 和 jenkins 中的对应源码,供大家参考。
如果面试时,不仅能写出对应的代码,还能讲出 kafka 和 jenkis 中的实现的异同,也能够增色不少,超出预期。
2.2.1 kafka
仓库地址:https://github.com/apache/kafka
org.apache.kafka.common.utils.CopyOnWriteMap
源码:
package org.apache.kafka.common.utils;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
/**
* A simple read-optimized map implementation that synchronizes only writes and does a full copy on each modification
*/
public class CopyOnWriteMap<K, V> implements ConcurrentMap<K, V>
private volatile Map<K, V> map;
public CopyOnWriteMap()
this.map = Collections.emptyMap();
public CopyOnWriteMap(Map<K, V> map)
this.map = Collections.unmodifiableMap(map);
@Override
public boolean containsKey(Object k)
return map.containsKey(k);
@Override
public boolean containsValue(Object v)
return map.containsValue(v);
@Override
public Set<java.util.Map.Entry<K, V>> entrySet()
return map.entrySet();
@Override
public V get(Object k)
return map.get(k);
@Override
public boolean isEmpty()
return map.isEmpty();
@Override
public Set<K> keySet()
return map.keySet();
@Override
public int size()
return map.size();
@Override
public Collection<V> values()
return map.values();
@Override
public synchronized void clear()
this.map = Collections.emptyMap();
@Override
public synchronized V put(K k, V v)
Map<K, V> copy = new HashMap<>(this.map);
V prev = copy.put(k, v);
this.map = Collections.unmodifiableMap(copy);
return prev;
@Override
public synchronized void putAll(Map<? extends K, ? extends V> entries)
Map<K, V> copy = new HashMap<>(this.map);
copy.putAll(entries);
this.map = Collections.unmodifiableMap(copy);
@Override
public synchronized V remove(Object key)
Map<K, V> copy = new HashMap<>(this.map);
V prev = copy.remove(key);
this.map = Collections.unmodifiableMap(copy);
return prev;
@Override
public synchronized V putIfAbsent(K k, V v)
if (!containsKey(k))
return put(k, v);
else
return get(k);
@Override
public synchronized boolean remove(Object k, Object v)
if (containsKey(k) && get(k).equals(v))
remove(k);
return true;
else
return false;
@Override
public synchronized boolean replace(K k, V original, V replacement)
if (containsKey(k) && get(k).equals(original))
put(k, replacement);
return true;
else
return false;
@Override
public synchronized V replace(K k, V v)
if (containsKey(k))
return put(k, v);
else
return null;
该源码的实现非常简单:
(1)写方法上都加上了 synchronized
关键字确保线程安全;
(2)修改前都通过 Map<K, V> copy = new HashMap<>(this.map);
拷贝出一个新的 map, 符合“写时复制” 的思想;
对于 map 采用 Collections.unmodifiableMap
构造不可修改的 Map
是一大亮点,是代码健壮性的体现。
可以看到 java.util.Collections#unmodifiableMap
会构造 UnmodifiableMap
。
/**
* Returns an <a href="Collection.html#unmodview">unmodifiable view</a> of the
* specified map. Query operations on the returned map "read through"
* to the specified map, and attempts to modify the returned
* map, whether direct or via its collection views, result in an
* @code UnsupportedOperationException.<p>
*
* The returned map will be serializable if the specified map
* is serializable.
*
* @implNote This method may return its argument if the argument is already unmodifiable.
* @param <K> the class of the map keys
* @param <V> the class of the map values
* @param m the map for which an unmodifiable view is to be returned.
* @return an unmodifiable view of the specified map.
*/
@SuppressWarnings("unchecked")
public static <K,V> Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m)
// Not checking for subclasses because of heap pollution and information leakage.
if (m.getClass() == UnmodifiableMap.class)
return (Map<K,V>) m;
return new UnmodifiableMap<>(m);
包装后的 UnmodifiableMap
的 keySet
、values
、entrySet
获取的集合等都是不可编辑的。
private static class UnmodifiableMap<K,V> implements Map<K,V>, Serializable
public Set<K> keySet()
if (keySet==null)
keySet = unmodifiableSet(m.keySet());
return keySet;
public Set<Map.Entry<K,V>> entrySet()
if (entrySet==null)
entrySet = new UnmodifiableEntrySet<>(m.entrySet());
return entrySet;
public Collection<V> values()
if (values==null)
values = unmodifiableCollection(m.values());
return values;
确保上游通过CopyOnWriteMap
的 keySet
、values
、entrySet
等方法获取到对应的集合是只读的,能够保证集合(并不是集合元素,只是集合本身)不会被修改(如新增、删除、替换、清空等)从而保证只能统一使用 CopyOnWriteMap
本身的写操作的 API 对 map 进行修改,由于写操作都有同步关键字,这样就能够保证线程安全。
注意它实现了 ConcurrentMap
接口。
还需要注意的是,属性中的 map 加了 volatile
关键字,这也是面试官考察的重点。
2.2.2 jenkis 实现
仓库地址:https://github.com/jenkinsci/jenkins
hudson.util#CopyOnWriteMap
源码:
package hudson.util;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.converters.collections.MapConverter;
import com.thoughtworks.xstream.converters.collections.TreeMapConverter;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.mapper.Mapper;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap以上是关于面试官:请手写一个 CopyOnWriteMap!的主要内容,如果未能解决你的问题,请参考以下文章