Kafka源码解析(八)- 高性能数据结构CopyOnWriteMap

上一篇介绍了将数据存入RecordAccumulator 这一过程

这段代码是在高并发的情况下运行的,既然如此,就会有线程安全的问题。我们看到作者设计这段代码的时候,并没有在整个方法上加锁,而是采用分段解锁的方式,将性能提升到最高
但是不知道大家有没有注意到获取或创建队列的时候,也就是这行代码 Deque dq = getOrCreateDeque(tp); 是没有加锁的,那没有线程安全的问题吗?
答案是没有!在这儿:

我们看到,进来之后第一件事儿就是从batches里面获取队列,那么batches究竟是怎么实现的呢?

在初始化的时候,new了一个CopyOnWriteMap,这个是kafka自己设计的数据结构。看一下这个数据结构里面是怎么做的

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<K, V>(this.map);
    //插入数据
    V prev = copy.put(k, v);
    //合并赋值给map
    this.map = Collections.unmodifiableMap(copy);
    return prev;
}

@Override
public synchronized void putAll(Map<? extends K, ? extends V> entries) {
    Map<K, V> copy = new HashMap<K, V>(this.map);
    copy.putAll(entries);
    this.map = Collections.unmodifiableMap(copy);
}

@Override
public synchronized V remove(Object key) {
    Map<K, V> copy = new HashMap<K, V>(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;
    }
}

看这样一个数据结构最主要是看他的get 和 put 方法,我们可以看到,get方法就是从map里面去取数据,没有加锁。那么看set方法

@Override
public synchronized V put(K k, V v) {
    //新的内存空间
    Map<K, V> copy = new HashMap<K, V>(this.map);
    //插入数据
    V prev = copy.put(k, v);
    //合并赋值给map
    this.map = Collections.unmodifiableMap(copy);
    return prev;
}

往map里面put数据的时候,首先开辟一块新的内存空间,然后将数据存到里面,最后将新开辟的空间和之前的map数据合并,最后赋值给map
这样做到了读写分离的效果,读和写互不影响,而且private volatile Map<K, V> map; 这个map是用volatile修饰的,保证了多线程的可见性。
因此,在获取或创建队列的这里块,也是线程安全的。

那么这个高性能的数据结构应用于什么场景呢? 答案是:读多写少的场景!

我们知道getOrCreateDeque 这一过程首先是要从batches里面获取队列,这一动作就是“读”,而每次进来都要get一下,也就是说每发送一条数据都要读一次
而put这一动作,仅在从batches里面没有获取到队列的时候,才会创建,并将队列这个对象put导batches里面,也就是触发“写”动作,因此是读多写少的场景。

posted @ 2020-11-04 00:57  写写代码睡着了  阅读(47)  评论(0)    收藏  举报