iis服务器助手广告广告
返回顶部
首页 > 资讯 > 精选 >如何理解Java容器中Map的源码分析
  • 840
分享到

如何理解Java容器中Map的源码分析

2023-06-05 04:06:45 840人浏览 泡泡鱼
摘要

本篇文章为大家展示了如何理解Java容器中Map的源码分析,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。如果没有特别说明,以下源码分析基于 jdk 1.8。一、HashMap为了便于理解,以下源码分

本篇文章为大家展示了如何理解Java容器中Map的源码分析,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

如果没有特别说明,以下源码分析基于 jdk 1.8。

一、HashMap

为了便于理解,以下源码分析以 JDK 1.7 为主。

1. 存储结构

内部包含了一个 Entry 类型的数组 table。

transient Entry[] table;

Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。 即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突, 同一个链表中存放哈希值相同的 Entry。

如何理解Java容器中Map的源码分析

static class Entry<K,V> implements Map.Entry<K,V> {    //包含了四个字段    final K key;    V value;    //next指向下一个节点,说明是链表结构    Entry<K,V> next;    int hash;    Entry(int h, K k, V v, Entry<K,V> n) {        value = v;        next = n;        key = k;        hash = h;    }    public final K geTKEy() {        return key;    }    public final V getValue() {        return value;    }    public final V setValue(V newValue) {        V oldValue = value;        value = newValue;        return oldValue;    }    public final Boolean equals(Object o) {        if (!(o instanceof Map.Entry))                    return false;        Map.Entry e = (Map.Entry)o;        Object k1 = getKey();        Object k2 = e.getKey();        // k1==k2 比较的是 hashcode 值,        // k1.equals(k2)比较的是k1和k2的内容 equals 未重写,则等价于 k1 == k2        if (k1 == k2 || (k1 != null && k1.equals(k2))) {            Object v1 = getValue();            Object v2 = e.getValue();            if (v1 == v2 || (v1 != null && v1.equals(v2)))                            return true;        }        return false;    }    public final int hashCode() {        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());    }    public final String toString() {        return getKey() + "=" + getValue();    }}

2. 拉链法的工作原理

HashMap<String, String> map = new HashMap<>();map.put("K1", "V1");map.put("K2", "V2");map.put("K3", "V3");
  • 新建一个 HashMap,默认大小为 16;

  • 插入

    键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。
  • 插入

    键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
  • 插入

    键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在前面。

应该注意到链表的插入是以头插法方式进行的,例如上面的不是插在后面,而是插入在链表头部。

查找需要分成两步进行:

  • 计算键值对所在的桶;

  • 在链表上顺序查找,时间复杂度显然和链表的长度成正比。

3. put 操作

public V put(K key, V value) {    if (table == EMPTY_TABLE) {        inflateTable(threshold);    }    // 键为 null 单独处理    if (key == null)            return putForNullKey(value);    int hash = hash(key);    // 确定桶下标    int i = indexFor(hash, table.length);    // 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value    // 时间复杂度显然和链表的长度成正比。    for (Entry<K,V> e = table[i]; e != null; e = e.next) {        Object k;        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {            V oldValue = e.value;            e.value = value;            e.recordAccess(this);            return oldValue;        }    }    modCount++;    // 插入新键值对    addEntry(hash, key, value, i);    return null;}

HashMap 允许插入键为 null 的键值对。但是因为无法调用 null 的 hashCode() 方法,也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。

private V putForNullKey(V value) {    //HashMap 使用第 0 个桶 table[0] 存放键为 null 的键值对。    for (Entry<K,V> e = table[0]; e != null; e = e.next) {        if (e.key == null) {            V oldValue = e.value;            e.value = value;            // 更新值            e.recordAccess(this);            return oldValue;            // 返回旧值        }    }    modCount++;    //void addEntry(int hash, K key, V value, int bucketIndex)    addEntry(0, null, value, 0);    return null;}

使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。

//TODO:使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。void addEntry(int hash, K key, V value, int bucketIndex) {    if ((size >= threshold) && (null != table[bucketIndex])) {        resize(2 * table.length);        hash = (null != key) ? hash(key) : 0;        bucketIndex = indexFor(hash, table.length);    }    createEntry(hash, key, value, bucketIndex);}void createEntry(int hash, K key, V value, int bucketIndex) {    Entry<K,V> e = table[bucketIndex];    // 头插法,链表头部指向新的键值对    table[bucketIndex] = new Entry<>(hash, key, value, e);    size++;}
Entry(int h, K k, V v, Entry<K,V> n) {    value = v;    next = n;    key = k;    hash = h;}

4. 确定桶下标

很多操作都需要先确定一个键值对所在的桶下标。

int hash = hash(key);int i = indexFor(hash, table.length);

①. 计算 hash 值

final int hash(Object k) {    int h = hashSeed;    if (0 != h && k instanceof String) {        return sun.misc.Hashing.stringHash42((String) k);    }    h ^= k.hashCode();    // This function ensures that hashCodes that differ only by    // constant multiples at each bit position have a bounded    // number of collisions (approximately 8 at default load factor).    h ^= (h >>> 20) ^ (h >>> 12);    return h ^ (h >>> 7) ^ (h >>> 4);}
public final int hashCode() {    return Objects.hashCode(key) ^ Objects.hashCode(value);}

②. 取模

令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:

x   : 00010000x-1 : 00001111

令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:

y       : 10110010x-1     : 00001111y&(x-1) : 00000010

这个性质和 y 对 x 取模效果是一样的:

y   : 10110010x   : 00010000y%x : 00000010

我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时用位运算的话能带来更高的性能。

确定桶下标的最后一步是将 key 的 hash 值对桶个数取模: hash%capacity,如果能保证 capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运算。

static int indexFor(int h, int length) {    return h & (length-1);}

就等价于

static int indexFor(int h, int length) {    return h % length;}

但是效率会更高。

5. 扩容-基本原理

设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。

为了让查找的成本降低,应该尽可能使得 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。 HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。

和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。

如何理解Java容器中Map的源码分析

static final int DEFAULT_INITIAL_CAPACITY = 16;//保证 capacity 为 2 的 n 次方,那么就可以将indexFor方法中操作转换为位运算static final int MAXIMUM_CAPACITY = 1 << 30;//保证 capacity 为 2 的 n 次方,那么就可以将 indexFor 方法中操作转换为位运算static final float DEFAULT_LOAD_FACTOR = 0.75f;transient Entry[] table;transient int size;int threshold;final float loadFactor;transient int modCount;

从下面的添加元素代码中可以看出,当需要扩容时,令 capacity 为原来的两倍。

void addEntry(int hash, K key, V value, int bucketIndex) {    Entry<K,V> e = table[bucketIndex];    table[bucketIndex] = new Entry<>(hash, key, value, e);    if (size++ >= threshold)            resize(2 * table.length);    //令 capacity 为原来的两倍}

扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。

void resize(int newCapacity) {    Entry[] oldTable = table;    int oldCapacity = oldTable.length;    if (oldCapacity == MAXIMUM_CAPACITY) {        threshold = Integer.MAX_VALUE;        return;    }    Entry[] newTable = new Entry[newCapacity];    transfer(newTable);    table = newTable;    threshold = (int)(newCapacity * loadFactor);}void transfer(Entry[] newTable) {    Entry[] src = table;    int newCapacity = newTable.length;    for (int j = 0; j < src.length; j++) {        Entry<K,V> e = src[j];        if (e != null) {            src[j] = null;            do {                Entry<K,V> next = e.next;                int i = indexFor(e.hash, newCapacity);                e.next = newTable[i];                newTable[i] = e;                e = next;            }            while (e != null);        }    }}

6. 扩容-重新计算桶下标

在进行扩容时,需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以提升重新计算桶下标的效率。

假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32:

capacity     : 00010000new capacity : 00100000

对于一个 Key,

  • 它的哈希值如果在第 5 位上为 0,那么取模得到的结果和之前一样;

  • 如果为 1,那么得到的结果为原来的结果 +16。

7. 计算数组容量

HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。

先考虑如何求一个数的掩码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到:

mask |= mask >> 1    11011000mask |= mask >> 2    11111110mask |= mask >> 4    11111111

mask+1 是大于原始数字的最小的 2 的 n 次方。

num     10010000mask+1  100000000

以下是 HashMap 中计算数组容量的代码:

static final int tableSizeFor(int cap) {    int n = cap - 1;    n |= n >>> 1;    n |= n >>> 2;    n |= n >>> 4;    n |= n >>> 8;    n |= n >>> 16;    //得到n的掩码    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

8. 链表转红黑树

从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。

9. 与 HashTable 的比较

  • HashMap 是非线程安全的,HashTable 使用 synchronized 来进行同步,是线程安全的。

  • HashMap 要比 HashTable 效率高一点。Hashtable 基本被淘汰,不要在代码中使用它。

  • HashMap 可以插入键为 null 的 Entry;HashTable 中插入的键只要有一个为 null,直接抛出 NullPointerException。

  • HashMap 的迭代器是 fail-fast 迭代器。

  • HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。

  • HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间;Hashtable 没有这样的机制。

  • HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍;Hashtable 容量默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。 在初始化时如果给定了容量初始值,HashMap 会将其扩充为2的幂次方大小;Hashtable 会直接使用初始值。

10. 与 HashSet 的比较

HashSet 底层就是基于HashMap实现的。 (HashSet 的源码非常非常少,因为除了 clone() 方法、writeObject()方法、readObject()方法是 HashSet 自己不得不实现之外, 其他方法都是直接调用 HashMap 中的方法。)

如何理解Java容器中Map的源码分析

二、LinkedHashMap

1.存储结构

继承自 HashMap,因此具有和 HashMap 一样的快速查找特性。

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。

transient LinkedHashMap.Entry<K,V> head;transient LinkedHashMap.Entry<K,V> tail;

accessOrder 决定了顺序,默认为 false,此时维护的是插入顺序。

final boolean accessOrder;

LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。

void afternodeAccess(Node<K,V> p) { }void afterNodeInsertion(boolean evict) { }

2.afterNodeAccess()

当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。

void afterNodeAccess(Node<K,V> e) {    // move node to last    LinkedHashMap.Entry<K,V> last;    if (accessOrder && (last = tail) != e) {        LinkedHashMap.Entry<K,V> p =                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;        p.after = null;        if (b == null)                    head = a; else                    b.after = a;        if (a != null)                    a.before = b; else                    last = b;        if (last == null)                    head = p; else {            p.before = last;            last.after = p;        }        tail = p;        ++modCount;    }}

3.afterNodeInsertion()

在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。

evict 只有在构建 Map 的时候才为 false,在这里为 true。

void afterNodeInsertion(Boolean evict) {    // possibly remove eldest    LinkedHashMap.Entry<K,V> first;    if (evict && (first = head) != null && removeEldestEntry(first)) {        K key = first.key;        removeNode(hash(key), key, null, false, true);    }}

removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {    return false;}

4.LRU 缓存

以下是使用 LinkedHashMap 实现的一个 LRU 缓存:

  • 设定最大缓存空间 MAX_ENTRIES 为 3;

  • 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;

  • 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。

public class LRUCache<K,V> extends LinkedHashMap<K,V>{    private static final int MAX_ENTRIES = 3;    LRUCache(){        super(MAX_ENTRIES,0.75f,true);    }        @Override        protected Boolean removeEldestEntry(Map.Entry eldest) {        return size() > MAX_ENTRIES;    }    public static void main(String[] args) {        LRUCache<Integer,String> cache=new LRUCache<>();        cache.put(1, "a");        cache.put(2, "b");        cache.put(3, "c");        cache.get(1);        //LRU  键值1被访问过了,则最近最久未访问的就是2        cache.put(4, "d");        System.out.println(cache.keySet());    }}
[3, 1, 4]

三、WeakHashMap

1.存储结构

WeakHashMap 的 Entry 继承自 WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。

WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>

2.ConcurrentCache

Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。

ConcurrentCache 采取的是分代缓存:

  • 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收;

  • 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。

  • 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收。

  • 当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。

public final class ConcurrentCache<K, V> {    private final int size;    private final Map<K, V> eden;    private final Map<K, V> longterm;    public ConcurrentCache(int size) {        this.size = size;        this.eden = new ConcurrentHashMap<>(size);        this.longterm = new WeakHashMap<>(size);    }    public V get(K k) {        V v = this.eden.get(k);        if (v == null) {            v = this.longterm.get(k);            if (v != null)                            this.eden.put(k, v);        }        return v;    }    public void put(K k, V v) {        if (this.eden.size() >= size) {            this.longterm.putAll(this.eden);            this.eden.clear();        }        this.eden.put(k, v);    }}

上述内容就是如何理解Java容器中Map的源码分析,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注编程网精选频道。

--结束END--

本文标题: 如何理解Java容器中Map的源码分析

本文链接: https://www.lsjlt.com/news/241070.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 如何理解Java容器中Map的源码分析
    本篇文章为大家展示了如何理解Java容器中Map的源码分析,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。如果没有特别说明,以下源码分析基于 JDK 1.8。一、HashMap为了便于理解,以下源码分...
    99+
    2023-06-05
  • 如何理解Java容器中ArrayList的源码分析
    这篇文章给大家介绍如何理解Java容器中List的源码分析,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。如果没有特别说明,以下源码分析基于 JDK 1.8。一、ArrayList1. 概览实现了 RandomAcces...
    99+
    2023-06-05
  • 如何理解Java 容器中并发容器的源码分析
    这期内容当中小编将会给大家带来有关如何理解Java 容器中并发容器的源码分析,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。如果没有特别说明,以下源码分析基于 JDK 1.8。CopyOnWriteArra...
    99+
    2023-06-05
  • Spring的Model 和 Map的原理源码解析
    Model 和 Map 为什么在Model和Map中放值传入后会出现在request的上面。 9.1、源码解析 准备测试代码 @GetMapping("/goto") public ...
    99+
    2024-04-02
  • 源码剖析Golang中map扩容底层的实现
    目录前言map底层结构扩容时机条件1:超过负载条件2:溢出桶太多扩容方式双倍扩容等量扩容扩容函数总结前言 之前的文章详细介绍过Go切片和map的基本使用,以及切片的扩容机制。本文针对...
    99+
    2023-03-06
    Golang map扩容实现 Golang map扩容 Golang map
  • java 中Buffer源码的分析
    java 中Buffer源码的分析BufferBuffer的类图如下:除了Boolean,其他基本数据类型都有对应的Buffer,但是只有ByteBuffer才能和Channel交互。只有ByteBuffer才能产生Direct的buffe...
    99+
    2023-05-31
    java buffer源码 buf
  • Java中的CyclicBarrier源码分析
    这篇文章主要介绍了Java中的CyclicBarrier源码分析的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Java中的CyclicBarrier源码分析文章都会有所收获,下面我们一起来看看吧。CyclicB...
    99+
    2023-06-30
  • Java ConcurrentHashMap的源码分析详解
    目录概述ForwardingNode节点TreeNodeTreeBinSizeCtl初始化初始化流程查找插入扩容红黑树的读&写读操作写操作小结容器计数总结概述 Concurr...
    99+
    2023-03-02
    Java ConcurrentHashMap源码 Java ConcurrentHashMap
  • 源码分析Java中ThreadPoolExecutor的底层原理
    目录一、根据代码查看jdk提供的3种线程池创建二、3种方式源码分析1、Executors.newCachedThreadPool()2、Executors.newFixedThrea...
    99+
    2023-05-19
    Java ThreadPoolExecutor原理 Java ThreadPoolExecutor
  • Java源码解析之ConcurrentHashMap的示例分析
    小编给大家分享一下Java源码解析之ConcurrentHashMap的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!早期 ConcurrentHashMap,其实现是基于:分离锁,也就是将内部进行分段(Segme...
    99+
    2023-06-15
  • Java中Handler源码的示例分析
    这篇文章主要介绍了Java中Handler源码的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。从很早开始就认识到 Handler 了,只不过那时修为尚浅,了解的不够深...
    99+
    2023-06-02
  • java中CopyOnWriteArrayList源码的示例分析
    这篇文章将为大家详细讲解有关java中CopyOnWriteArrayList源码的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。简介CopyOnWriteArrayList是ArrayList的...
    99+
    2023-06-29
  • RxJava中map和flatMap的用法区别源码解析
    目录前言:作用使用方法:mapflatMap源码分析mapflatMap结语前言: RxJava中提供了大量的操作符,这大大提高了了我们的开发效率。其中最基本的两个变换操作符就是ma...
    99+
    2024-04-02
  • 如何进行HashMap扩容机制源码分析
    这期内容当中小编将会给大家带来有关如何进行HashMap扩容机制源码分析,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。具体看源码之前,我们先简单的说一下HashMap的底层数据结构  1、HashMap底...
    99+
    2023-06-02
  • 如何用源码分析Java HashMap实例
    本篇文章为大家展示了如何用源码分析Java HashMap实例,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。引言HashMap在键值对存储中被经常使用,那么它到底是如何实现键值存储的呢?一 Entr...
    99+
    2023-06-17
  • Java定时器Timer的源码分析
    目录一、TimerTask1. 任务状态2. 任务属性说明3. 任务方法说明二、Timer1. sched方法2. cancel方法3. purge方法三、TaskQueue四、Ti...
    99+
    2022-11-13
    Java Timer源码 Java Timer定时器 Java Timer
  • Java详解HashMap实现原理和源码分析
    目录学习要点:1、什么是HashMap?2、HashMap的特性3、HashMap的数据结构4、HashMap初始化操作4.1、成员变量4.2、 构造方法5、Jdk8中HashMap...
    99+
    2024-04-02
  • C++深入分析STL中map容器的使用
    目录1、map容器2、map容器原理3、map容器函数接口4、使用示例1、map容器 map是C++ STL的一个关联容器,它提供一对一的数据处理能力。其中,各个键值对的键和值可以是...
    99+
    2024-04-02
  • 如何解析hanlp源码中文分词算法
    如何解析hanlp源码中文分词算法,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。  解析hanlp源码中文分词算法。词图指的是...
    99+
    2024-04-02
  • Libtask源码解析之如何理解锁
    这篇文章主要讲解了“Libtask源码解析之如何理解锁”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Libtask源码解析之如何理解锁”吧!libtask中...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作