ConcurrentHashMap扩容原理 | 存储流程 | 源码探究

news2024/11/12 0:12:41

新人写手,代码菜鸡;笔下生涩,诚惶诚恐。

初试锋芒,尚显青涩;望君指点,愿受教诲。 

本篇文章将从源码的层面,探讨ConcurrentHashMap的存储流程以及扩容原理

Java版本为JDK17,源代码可能与其他版本略有不同

推荐阅读:HashMap实现原理、扩容机制

 一、构造函数

1.1 无参构造函数

ConcurrentHashMap的无参构造函数是一个空方法

public ConcurrentHashMap() {
}

1.2 指定容量、负载因子

多了一个并发级别concurrencyLevel,没看出来大用途emm......

public ConcurrentHashMap(int initialCapacity,
                         float loadFactor, int concurrencyLevel) {
    if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
        throw new IllegalArgumentException();
    if (initialCapacity < concurrencyLevel)   // Use at least as many bins
        initialCapacity = concurrencyLevel;   // as estimated threads
    long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    // 找到大于等于 容量 / 负载因子 + 1 的2的幂
    int cap = (size >= (long)MAXIMUM_CAPACITY) ?
        MAXIMUM_CAPACITY : tableSizeFor((int)size);
    this.sizeCtl = cap;
}

1.3 传入Map

public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
    this.sizeCtl = DEFAULT_CAPACITY;
    putAll(m);
}

二、关键成员变量

扩容相关的变量都用到了volatile关键字作修饰,保证变量并发可见性

// 默认大小,树化阈值等与HashMap相同
// ...
// 存储元素的Node数组
transient volatile Node<K,V>[] table;
// 协助扩容会用到
private transient volatile Node<K,V>[] nextTable;
// 大于0时,为扩容阈值,等于0时,为初始状态,小于0时,表示Map处于扩容中等一些特殊状态
// 扩容时,该值的低十六位用于表示共同扩容线程数
private transient volatile int sizeCtl;
// 扩容下标指针,用于记录数组扩容到了哪一个元素,从数组末尾开始
private transient volatile int transferIndex;
// 使用Unsafe高效读取和修改变量值
private static final Unsafe U = Unsafe.getUnsafe();
private static final long SIZECTL
        = U.objectFieldOffset(test.ConcurrentHashMap.class, "sizeCtl");
private static final long TRANSFERINDEX
        = U.objectFieldOffset(test.ConcurrentHashMap.class, "transferIndex");
// 扩容标志位数
private static final int RESIZE_STAMP_BITS = 16;
// 最大允许扩容线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 扩容标志位左移位数,扩容时用于将sizeCtl置为负数
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// 表示节点的状态,一般扩容时会将正在扩容的节点hash值修改为以下值
static final int MOVED     = -1; // hash for forwarding nodes
static final int TREEBIN   = -2; // hash for roots of trees
static final int RESERVED  = -3; // hash for transient reservations
// 系统可用线程数,用于计算扩容步长
static final int NCPU = Runtime.getRuntime().availableProcessors();

三、存储流程

3.1 put流程

put方法会调用putVal方法,业务逻辑都在putVal中

public V put(K key, V value) {
    return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // key 和 value都不能为null
    if (key == null || value == null) throw new NullPointerException();
    // key 的 hash 值
    int hash = spread(key.hashCode());
    int binCount = 0;
    // 并发场景基本都用到了for循环,执行插入时,若map正在扩容导致插入失败
    // 待扩容完成后可在此尝试插入
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh; K fk; V fv;
        // table为null或长度为0,执行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 如果当前table数组i位置没有元素,直接以cas方式插入当前待插入节点
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;                   // no lock when adding to empty bin
        }
        // 如果当前当前节点元素hash值为-1,表示正在扩容(扩容时,扩容节点hash值会置为-1)
        else if ((fh = f.hash) == MOVED)
            // 则辅助进行扩容,具体后面说
            tab = helpTransfer(tab, f);
        // 走到这里,说明table[i]位置节点存在,并且数据不在扩容
        // 和HashMap类似,onlyIfAbsent为true时,
        // 仅当节点不存在时,才会插入;
        // 如果存在,不会更新节点值
        // 同样的,判断节点相同,需要hash值和key值都一致
        else if (onlyIfAbsent // check first node without acquiring lock
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                 && (fv = f.val) != null)
            return fv;
        else {
            V oldVal = null;
            // 锁住当前节点
            synchronized (f) {
                // 再次判断当前节点是否发生了变化,防止被其他线程提前修改
                if (tabAt(tab, i) == f) {
                    // fh < 0时,代表三个特殊状态
                    // static final int MOVED     = -1; 当前节点正在扩容
                    // static final int TREEBIN   = -2; 当前节点是一颗树
                    // static final int RESERVED  = -3; 保留当前位置
                    // 这里fh >= 0表示当前节点f为单节点(不是树或链表),或者为链表
                    if (fh >= 0) {
                        // 链表长度
                        binCount = 1;
                        // 开始寻找待插入节点在链表中的位置
                        // 找不到则直接挂载到链表尾部
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 找到了,直接更新数据(当然onlyIfAbsent为false)
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            // 如果链表最后都没有找到,新建一个Node挂到后面
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key, value);
                                break;
                            }
                        }
                    }
                    // 如果为树,则遍历树,更新或插入节点
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                    // 防止递归更新产生死锁
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            if (binCount != 0) {
                // 如果链表长度大于等于8,尝试将链表转化为树
                // 只有table容量大于等于64才会尝试转化,否则优先扩容
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 计数+1,并检查是否需要扩容(是否达到扩容阈值)
    addCount(1L, binCount);
    return null;
}
  • 由于需要保证线程安全,所以并不是每一个插入操作都是一次成功(如插入时Map正在扩容),所以用到了for循环;
  • 插入元素时,用到了Synchronized锁,锁粒度具体到Node[]数组中的某个Node节点;
  • 加锁后又判断了一次if (tabAt(tab, i) == f),类似双重校验锁,ConcurrentHashMap大量使用到了这种处理方式。

3.2 get流程

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // 获取hash值
    int h = spread(key.hashCode());
    // 确保能查到元素,否则直接返回null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // hash和key都相同,返回值
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 节点hash值小于0,表示可能处于扩容状态
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        // 遍历链表,继续查找key相同的节点,返回值
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

// 类似,遍历链表查找key
Node<K,V> find(int h, Object k) {
    Node<K,V> e = this;
    if (k != null) {
        do {
            K ek;
            if (e.hash == h &&
                ((ek = e.key) == k || (ek != null && k.equals(ek))))
                return e;
        } while ((e = e.next) != null);
    }
    return null;
}

四、扩容原理

在说传参为Map的构造函数的时候,见到过这样的方法

public void putAll(Map<? extends K, ? extends V> m) {
    tryPresize(m.size());
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
        putVal(e.getKey(), e.getValue(), false);
}

接下来就从tryPresize方法入手

4.1 tryPresize 尝试扩容

private final void tryPresize(int size) {
    // 如果size >= max / 2,扩容后容量就用max
    // 否则使用1.5 * size + 1向上扩为2的幂次方
    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
        tableSizeFor(size + (size >>> 1) + 1);
    int sc;
    // sizeCtl >= 0表示不处于扩容等异常状态
    while ((sc = sizeCtl) >= 0) {
        Node<K,V>[] tab = table; int n;
        // putAll方法可能会执行此分支,table为空
        if (tab == null || (n = tab.length) == 0) {
            // 初始化长度sc够就用sc,否则用前面算出来的c
            n = (sc > c) ? sc : c;
            // cas把当前map状态置为扩容中,确保只有一个线程进入
            if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
                try {
                    // 确保table引用没被修改
                    // 创建新数组
                    // 将sizeCtl赋值为容量的0.75倍
                    if (table == tab) {
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
            }
        }
        // 数组容量已经大于所需要的容量,或者容量达到最大值
        // 无需扩容,直接退出循环
        else if (c <= sc || n >= MAXIMUM_CAPACITY)
            break;
        else if (tab == table) {
            // 获取扩容标志
            // 假设n = 16
            // 此时rs为1000 0000 0001 1011
            int rs = resizeStamp(n);
            // 扩容标志会使得低到高第16位为1,左移16位,保证sc为负数
            // 此时sc修改为1000 0000 0001 1011 0000 0000 0000 0010
            // 即-2145714174
            if (U.compareAndSetInt(this, SIZECTL, sc,
                                    (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
        }
    }
}
  • if (U.compareAndSetInt(this, SIZECTL, sc, -1))保证只有一个线程执行new Node[]操作
  • 扩容标志,用在协助扩容的时候,识别两个线程处于同一个扩容任务中

4.2 transfer 扩容方法

计算步长与初始化

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    // 每个线程迁移数据的步长,数组长度 / 8 / cpu可用线程数
    // 最小为16
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
    // 初始化走这
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            // 新建数组,容量为之前的两倍,并赋值
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        // 赋值给volatile变量
        // private transient volatile Node<K,V>[] nextTable;
        nextTable = nextTab;
        // private transient volatile int transferIndex;
        transferIndex = n;
    }
    // ...... 
}

线程接受扩容任务

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // ......
    // 新数组长度
    int nextn = nextTab.length;
    // 扩容节点,挂载的是新数组
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    // 任务标识符,为true时表示需要接受扩容任务
    boolean advance = true;
    // 结束标识符,为true时表示扩容结束
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        // 需要接受任务时
        while (advance) {
            int nextIndex, nextBound;
            // 第三个分支i被赋值后或扩容结束走此分支
            // 表示当前线程已接受任务
            if (--i >= bound || finishing)
                advance = false;
            // 数组所有节点扩容任务均分配完成
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            // 修改扩容下标,假设需要扩容的数组长度为32,步长16
            // 则每次循环transferIndex会减16,直到减为0
            // nextBound记录当前循环到的transferIndex下标
            // i赋值为nextIndex-1,(bound - i)即为当前线程需要数据迁移的数组下标
            // 接受过任务,将advance置为false,不再进入下一循环
            else if (U.compareAndSetInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;
                i = nextIndex - 1;
                advance = false;
            }
        }
    }
}

 bound -> i即为当前线程需要进行数据迁移的数组下标。循环结束时,若此时没有其他线程协助扩容,当前线程会再次接受新的一批迁移任务

扩容后处理 

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // ......
    // ......
    for (int i = 0, bound = 0;;) {
        // ......
        // 当前线程没有分配到任务时会满足i < 0条件
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            // 扩容结束,nextTable置为null
            // nextTab赋值给table
            // 阈值置为新数组容量的0.75倍
            // 第一次进入为false,之后finishing被赋值为true,执行finishing逻辑
            if (finishing) {
                nextTable = null;
                table = nextTab;
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            // cas使sizeCtl = sizeCtl - 1
            if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 第一个线程进入此,不会走下面的if
                // 进入transfer方法时,满足(sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
                // 第二次进入,sc = sizeCtl比原来的值少1,可以进入if条件,执行return;
                // 这里的判断,是为了检查所有辅助扩容线程是否均已扩容完毕
                // 每个协助扩容线程都会将sc + 1
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                // 扩容结束
                finishing = advance = true;
                // i重新赋值为数组长度,让最后一个线程检查数组是否迁移完毕,并赋值sc为新的扩容阈值
                i = n; // recheck before commit
            }
        }
        // ......
    }
}

数据迁移

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // ......
    // ......
    for (int i = 0, bound = 0;;) {
        // ......
        // ......
        // 走到之后的分支意味着线程接收到了任务
        // 如果数组i位置为null,不需要迁移,并将数组i位置hash置为MOVED,代表已经迁移过
        else if ((f = tabAt(tab, i)) == null)
            /*
             ForwardingNode(Node<K,V>[] tab) {
                 super(MOVED, null, null);
                 this.nextTable = tab;
             }
             fwd hash为-1,key、value均为null
             */
            advance = casTabAt(tab, i, null, fwd);
        // hash为-1,说明该节点已经迁移过
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        // 进入节点迁移逻辑
        else {
            // 上锁,锁的是node数组f节点
            synchronized (f) {
                // 校验引用
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    // 单节点或链表部分
                    if (fh >= 0) {
                        // 和HashMap类似,将节点hash与n进行按位与分为两组
                        // 拆成低位链表和高位链表
                        // 低位链表放在新数组i位置,高位链表则为n + i位置
                        int runBit = fh & n;
                        Node<K,V> lastRun = f;
                        // 这里的循环是为了找到链表最后面的节点族的第一个节点
                        // 即保证lastRun节点及其之后的节点同属于高/低位链表
                        // 如此设计,最后遍历链表遍历到lastRun即可,因为lastRun之后的节点都和lastRun同属于一批
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        // 先将最后的节点族赋值
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        }
                        else {
                            hn = lastRun;
                            ln = null;
                        }
                        // 再次遍历f节点链表,开始构建高/低位链表
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            // 除了lastRun及其之后的链表是尾插法,其余遍历的链表采用头插法
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        // 新数组赋值,并将旧书组i位置置为fwd,标记已经迁移
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    // 树节点同样分为高低位数
                    else if (f instanceof TreeBin) {
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null;
                        TreeNode<K,V> hi = null, hiTail = null;
                        int lc = 0, hc = 0;
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            }
                            else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        // 拆分成两棵树后,判断长度是否小于6,决定是否需要红黑数链表化
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                            (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                            (lc != 0) ? new TreeBin<K,V>(hi) : t;
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    // 递归更新抛出异常
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
        }
    }
}

这里的链表迁移与HashMap略有不同:

HashMap的高低位链表,相对顺序与原顺序相同;

而ConcurrentHashMap,lastRun及其之后的节点顺序保持一致,在链表尾部。其他的节点会以头插法的方式加入到新数组中,如下图,橙色1 4 5为低位,2 3 6 7为高位

4.3 helpTransfer 协助扩容

putVal方法中,当前节点hash为-1时,线程会协助进行扩容

// 如果当前当前节点元素hash值为-1,表示正在扩容(扩容时,扩容节点hash值会置为-1)
else if ((fh = f.hash) == MOVED)
    // 则辅助进行扩容,具体后面说
    tab = helpTransfer(tab, f);
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    // 旧数组不为空,并且当前f节点为ForwardingNode
    // 并且f.nextTable也不为空,transfer方法中,nextTable为新数组
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        // 扩容戳
        int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
        // 新老数组都是同一个,并且sizeCtl处于扩容状态,协助进行扩容
        // 不相同可能是扩容完成了,或者发生了新的扩容
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            // 扩容时,sizeCtl低16位代表扩容的线程数,首次进入时被设置为了2
            // 当此时同时扩容数达到上限,或者开始执行最终扩容检查时(最终检查时,sizeCtl == rs + 1)
            // 或者扩容下标为0(任务全部分配)时,退出
            if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                transferIndex <= 0)
                break;
            // sizeCtl + 1,表示此时扩容线程+1
            if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
                // 协助扩容
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

transfer方法中,将nextTab赋值给了volatile变量nextTable,旧数组长度n赋值给了transferIndex,此处用来判断是否是同一个扩容任务以及快速判断扩容是否完成。

结语:相对于HashMap,ConcurrentHashMap源码稍微绕一点。本文起到辅助理解作用,其他方法(remove等)原理类似,感兴趣可自行阅读源码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2084976.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ollama 本地部署

文章目录 前言一、Ollama是什么&#xff1f;二、使用步骤1.安装 OllamaWindows检验是否安装成功 2.运行ollama 模型库运行模型提问修改配置&#xff08;可选&#xff09;如果有个性化需求&#xff0c;需要修改默认配置&#xff1a; 参考 前言 Ollama是一个易于使用的本地大模型…

LivePortraitV3,支持图像驱动和区域控制,更精确的人像控制(WIN,MAC)

LivePortrait又又又又更新了&#xff01;这速度真是&#x1f44d;&#x1f3fb;&#x1f44d;&#x1f3fb; 【LivePortraitV3&#xff0c;支持图像驱动和区域控制&#xff0c;更精确的人像控制&#xff08;WIN&#xff0c;MAC&#xff09;】 https://www.bilibili.com/video/…

别暑气 迎秋意

今年长达40天的“三伏”结束啦&#xff01; 伏天过后&#xff0c;暑热逐渐消退&#xff0c;天气开始转凉&#xff0c;秋季逐渐来临。我们也该调整好生活节奏&#xff0c;去迎接收获季节的开始。 1、注意防寒保暖 天气逐渐转凉&#xff0c;昼夜温差不断增大&#xff0c;所以要…

Pytest自动化测试框架关联/参数化实战

关联 利用Python提供的fixture可以实现关联 实现步骤&#xff1a; 在case目录下&#xff0c;新建conftest.py文件&#xff0c;比如我们需要token&#xff0c;就在这个文件下定义一个公共的方法&#xff0c;调用登录接口并返回需要的token值&#xff08;注&#xff1a;公共的方…

【乐企】有关乐企能力测试接口对接-货物运输服务(详细)

1、前置条件&#xff0c;参考【乐企】有关乐企能力测试接口对接-基础版&#xff08;详细&#xff09; 2、接口文档 和基础版区别&#xff1a; 1、传参的时候添加了 用例编码&#xff1a;ylbm 2、发票上传接口的服务编码变化了&#xff1a;fwbm:HWYSFPSC 3、能力编码和用例编码…

Linux——安装软件(mysql)

一、应用部署&#xff1a; 安装软件 运行某个程序或者服务 安装软件包 dnf/yum 包安装工具官方网站提供的集成软件包源码编译安装 // 源码编译的步骤 只应用于编译型语言 对于解释性语言编写的程序 采用不用的方式打包 编译型语言编写的程序&#xff1a; nginx解释性语言…

Verilog刷题笔记63

找BUG 1、&#xff1a;Bug mux2 挑错&#xff1a; module top_module (input sel,input [7:0] a,input [7:0] b,output [7:0]out );assign out sel?a:b;endmodule结果正确&#xff1a; 原因: 1、输出out也应为8位 2、逻辑错误&#xff0c;&按位操作&#xff0c;需要将…

【可兼容的】protobuf、streamlit、transformers、icetk、cpm_kernels版本号

搞大模型训练的工作不可避免地需要很多库&#xff0c;但是非常讨厌的事情是这些库动不动就不兼容。最近在做文本分类训练的时候又遇到了这个问题&#xff0c;为了避免后面再安装包的时候把我之前的环境破坏了&#xff0c;所以特地来记录一下&#xff1a;protobuf、streamlit、t…

GD32F4xx---RTC初始化设置及闹钟方式实现秒中断讲解

GD32F4xx—RTC初始化设置及闹钟方式实现秒中断讲解 1、下载链接:源码工程 一、概述 GD32F4x的RTC例程网上资源较少,详细阅读用户手册后做出如下配置。RTC模块提供了一个包含日期(年/月/日)和时间(时/分/秒/亚秒)的日历功能。除亚秒用二进制码显示外,时间和日期都以BC…

欧科云链: Web3浪潮下合规是“必选项”, 技术创新成发展重点

如果说2023年将是Web3的监管与合规之年。那么2024年就是Web3发展里程碑之年。 自2023年&#xff0c;包括美国、日本、新加坡、迪拜、中国香港等全球多个国家和地区金融中心都先后宣布要成为Web3中心、虚拟资产中心&#xff0c;并努力在监管框架下推动Web3生态的技术创新。 放…

对新手的现货白银交易建议

近期现货白银价格表现十分不错&#xff0c;连续的上涨已经突破了30关口&#xff0c;这是一个重要的心理关口&#xff0c;受投资行情的吸引&#xff0c;很多新手现货白银交易者入场。那么&#xff0c;有没有一些对这些新手投资者的现货白银交易建议呢&#xff1f;下面我们就来讨…

通过Docker部署Nacos,以及Docker Desktop进行管理

目录 一.不需要持久化存储 1.启动容器 2.查看容器和镜像​ 3.容器管理 二.持久化存储启动mysql容器 1.创建docker卷 2.运行容器,指定卷 3.在nacos里面随便建个配置文件 4.停止并删除nacos容器 5.重新运行容器,并且挂载相同的卷,也就是上面第二步的命令 6.打开nacos并…

redis的紧凑列表ziplist、quicklist、listpack

文章目录 前言一、ziplist1.1 ziplist 查找复杂度高1.2 ziplist 连续更新风险 二、quicklist三、listpack 前言 当数据量较小时&#xff0c;Redis 会优先考虑用 ziplist 来存储 hash、list、zset&#xff0c;这么做可以有效的节省内存空间&#xff0c;因为 ziplist 是一块连续…

2024年用哪个思维导图软件好?这款在线工具堪称国产之光!

思维导图软件哪个好&#xff1f; 如今已经是2024年了&#xff0c;想做思维导图&#xff0c;面对琳琅满目的思维导图软件&#xff0c;哪一个才是最适合我们的呢&#xff1f; 在选用思维导图软件时&#xff0c;我们可能会综合考虑多个方面&#xff0c;譬如功能数量、操作易用性…

未来工作场所:知识中台与AI的融合

在快速迭代的未来工作场所&#xff0c;知识中台与AI的融合正引领着一场深刻的工作方式变革。这种融合不仅优化了企业的知识管理流程&#xff0c;还通过智能工具如AI问答、内容生成等&#xff0c;极大地提升了工作效率和决策质量。接下来&#xff0c;我们将以HelpLook AI知识库为…

【C/C++】C++类与对象基本概念(抽象与封装、构造函数、析构函数、静态、友元)

文章目录 七、类与对象基本概念抽象定义与声明访问控制类的实现与使用对象指针、this指针与对象引用构造函数析构函数拷贝构造函数 七、类与对象基本概念 抽象 抽象是相对&#xff0c;而非绝对的 在研究问题时&#xff0c;侧重点不同&#xff0c;可能会产生不同的抽象结果;解决…

解密低代码:持续更新的必要性与背后驱动力

在数字化转型的浪潮中&#xff0c;低代码&#xff08;Low-Code&#xff09;开发平台已经成为企业快速构建应用程序的重要工具。低代码平台通过图形化界面和少量手写代码&#xff0c;帮助开发者和业务人员在短时间内构建复杂应用。然而&#xff0c;随着技术的不断演进和业务需求…

【C#】Visual Studio代码格式化方法

1. 快捷键 选中内容后&#xff0c;先键入 ctrlk 再键入 ctrlf&#xff08;注意&#xff1a;Visual Studio中标注两个快捷键的都是这样使用&#xff09; 2. 工具栏 编辑 - 高级 - 设置选定内容的格式

mp3格式转换器免费版来袭,告别格式限制,音乐更自由!

当下&#xff0c;mp3格式可以说是音频文件的主流格式。无论是通过耳机、音箱还是车载音响&#xff0c;我们都在使用mp3格式来播放收听音乐。智能手机、平板电脑等移动设备上通常内置mp3播放器。mp3经常在视频剪辑中充当背景音乐和特效音效。 为什么mp3格式如此普遍&#xff1f…

PHP高效易用在线简单商城系统小程序源码

&#x1f680;高效易用的在线简单商城系统&#xff0c;让电商创业轻松启航&#x1f6cd;️ &#x1f308; 一键开店&#xff0c;轻松上手 还在为繁琐的电商开店流程头疼吗&#xff1f;高效易用的在线简单商城系统&#xff0c;让你告别复杂设置&#xff0c;一键开启你的电商之旅…