JDK8的 ConcurrentHashMap 源码分析

news2024/9/21 20:47:13

目录

1. 导读

2. ConcurrentHashMap 成员变量解读

3. ConcurrentHashMap 初始化

3.1 ConcurrentHashMap 无参构造源码解读

3.2 ConcurrentHashMap 带参构造源码解读

3.3 tableSizeFor 方法作用解读

3.4 ConcurrenthashMap初始化总结

4. ConcurrentHashMap 添加元素方法解读

4.1 put 源码解读

4.2 putVal 方法解读

3.3 initTable 初始化方法解读

4.4 put 添加元素方法总结概括

5. ConcurrentHashMap 树化操作何时进行?

6. ConcurrentHashMap 数组长度维护解读

7. ConcurrentHashMap 键和值能否为空?


1. 导读

我们都知道,HashMap 是我们在面试过程中经常被问到的一个点,而与 HashMap 并存的一个,就是 ConcurrentHashMap,它与HashMap最大的区别就是能在多线程的情况下保证线程安全,下面就从源码角度深入探究一下 ConcurrentHashMap 底层到底是什么样的,又是如何实现线程安全的。本贴难度会稍微高一些,建议各位在学习ConcurrentHashMap 源码之前,可以先学会看懂 HashMap 的底层源码逻辑,再来学习 ConcurrentHashMap ,这样会轻松非常多,因为它们两个本就差不多,主要是 ConcurrentHashMap 主要是能保证线程安全,在这里我会说的尽可能详细,代码的注释也都会标注清楚。

还有一篇文章讲的是 HashMap 的源码,各位同学有兴趣可以结合观看

HashMap 底层源码深度解读_程序猿ZhangSir的博客-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/m0_70325779/article/details/132015542?spm=1001.2014.3001.5501

2. ConcurrentHashMap 成员变量解读

在解读源码之前,有很多关键的变量需要各位记住,这些都是 ConcurrentHashMap 源码类中的一些重要属性,我已经列出来了,各位同学可以结合自己电脑上的IDEA源码结合观看,注释如下

// 这里1左移30位,表示 数组最大容量MAXIMUM_CAPACITY 为 2^30 即2的30次方,
// 这个容量与 HashMap 的最大容量是一样的
private static final int MAXIMUM_CAPACITY = 1 << 30;

// DEFAULT_CAPACITY = 16 表示的就是默认的数组初始容量为16
// 默认初始容量与HashMap的默认初始容量一样,都为16
private static final int DEFAULT_CAPACITY = 16;

// 这里LOAD_FACTOR 指加载因子为0.75,即当数组中加入的数据超过了当前容量的0.75倍时,
// 要进行数组的扩容,这一点与 HashMap 是一样的,加在因子都是0.75
private static final float LOAD_FACTOR = 0.75f;

// table 就是我们 ConcurrentHashMap 底层真正存贮数据的那个数组,名为table
// HashMap 底层的数组名字也叫 table,这个倒是无关大雅
transient volatile Node<K,V>[] table;

// 这个变量代表了数组的阈值长度64 
static final int MIN_TREEIFY_CAPACITY = 64;

// 这个变量代表了链表的长度阈值8,与上面的64紧密配合
// 当链表的长度大于8且数组的长度大于64,就会把链表树化,提高查找效率
// 这个转换成树的时机与HashMap 一样,都是数组长度大于等于64并且链表长度大于等于8时
// 链表转换成红黑树结构
static final int TREEIFY_THRESHOLD = 8;

sizeCtl 属性解读

想要读懂 ConcurrentHashMap 的源码,sizeCtl这个变量非常关键,所以我把它单独拿出来,在源码的很多方法中都会发现它的身影,一定一定一定要记住,这里我大致总结了 sizeCtl 的几种情况

(1)sizeCtl 为0,代表数组未初始化,且数组的初始容量为16;

(2)sizeCtl 为正数,如果数组未初始化,那么其记录的是数组的初始容量,如果数组已经初始化,那么记录的是数则的扩容阈值;

(3)sizeCtl 为 -1,表示数组正在进行初始化;

(4)sizeCtl 小于0,并且不是 -1,表示数组正在扩容,-(1+n),表示此时有n个线程正在共同完成数组的扩容操作。

3. ConcurrentHashMap 初始化

我们知道,在初始化对象的时候,可以采用无参构造创建对象,ConcurrentHashMap 也一样,可以使用空参构造不设置初始容量,也可以使用带参构造设置初始容量。

3.1 ConcurrentHashMap 无参构造源码解读

从无参构造源码也可以看出,在 ConcurrentHashMap 无参构造方法中,它没有做任何的动作;也就是说,采用无参构造创建 ConcurrentHashMap 时,底层并没有创建数组对象。(这里补充一点,创建数组对象的动作是在后续进行 put 操作添加元素时创建的,后面会说到)。

初始化源码上方有一句话,翻译过来就是"创建一个新数组,数组默认长度为16",也对应了上面我说到的,默认初始容量为16。

3.2 ConcurrentHashMap 带参构造源码解读

如下为 ConcurrentHashMap 的有参构造方法,设置一个初始容量

// 这里 initialCapacity 就是我们传入的初始容量
public ConcurrentHashMap(int initialCapacity) {
// 先做了一步判断,判断传入的初始值是否小于0,
        if (initialCapacity < 0)
// 若小于0抛出异常
            throw new IllegalArgumentException();
// 代码走到这里,说明初始容量大于等于0,三元运算符做进一步逻辑运算
// (initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) 
// 三元运算是在判断我们传入的初始容量是否大于等于最大容量的一半,
// 若大于最大容量的一半,则初始化容量为最大容量;
// 若不大于一半,执行tableSizeFor方法计算出初始容量
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
// 运行下面这一行说明初始容量小于最大容量的一半,通过 tableSizeFor 方法计算出初始容量
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
// 将计算出来的结果赋值给 sizeCtl,这个sizeCtl 需要记住,后面还会提到
        this.sizeCtl = cap;
    }

3.3 tableSizeFor 方法作用解读

这个方法可能大家看不太懂,我就这么说吧,这个方法的目的是返回一个 2的整数次幂的数。如2^4 = 16,2^5 = 32,2^6 = 64。

结合上述扩容方法和 tableSizeFor 方法,我们可以知道,当我们传入一个初始值的时候,实际计算出的结果是 (传入的值+传入值的一半+1)的结果向上取整并且必须是2的整数次幂,说到这里,各位应该明白了吧.

如果我们时传入的是32,那么计算出的初始容量就是 32 + 16 + 1 = 49,49不是2的整数次幂,向上取整最小为 64,所以初始容量为64而不是我们传入的32;

如果我们传入的是16,那么计算出的结果就是 16 + 8 + 1 = 25,25不是2的整数次幂,向上取整在最小为32,所以计算出的初始容量为32而不是我们传入的16;

3.4 ConcurrenthashMap初始化总结

总结上面的初始化源码分析,我们可以得到以下结论。

(1)ConcurrentHahMap 采用无参构造在底层什么都没有做,真正创建数组是在 put 第一个元素扩容的时候才创建数组的。

(2)ConcurrentHashMap 带参构造中如果我们传入的初始容量大于等于最大容量的一半,则实际集合容量会使用最大容量 2^30 ;如果传入的初始容量小于集合最大长度的一半,则实际计算出的容量是(传入的值 + 传入值的一半 + 1)的结果向上取整并且必须是2的整数次幂。例如传入32,是计算出的容量是64而不是32。

4. ConcurrentHashMap 添加元素方法解读

ConcurrenthashMap 添加元素需要调用 put 方法,下面我们就详细分析 put 方法的原理。

4.1 put 源码解读

ConcurrentHashMap 的put添加方法源码如下,这里 put 方法调用了一个 putVal 方法,没有做别的事情,下面跟进查看 putVal 方法源码

public V put(K key, V value) {
        return putVal(key, value, false);
    }

4.2 putVal 方法解读

final V putVal(K key, V value, boolean onlyIfAbsent) {
// 这里判断key和value是否有空值存在,若有则抛出异常
        if (key == null || value == null) throw new NullPointerException();
// 这里对 key 做了一系列哈希运算得到key的一个哈希值
        int hash = spread(key.hashCode());
// binCount 与后面数据长度的维护有关,这里暂时不用关心
        int binCount = 0;
// 这里的 for 循环是一个死循环,只要不进行break,会一直循环
// ConcurrentHashMap底层数组名字叫 table,然后将table赋值给对象tab
        for (Node<K,V>[] tab = table;;) {
// 创建一个节点对象 f,定义 n,i,fh 三个变量
            Node<K,V> f; int n, i, fh;
// 这里对tab做判空操作或长度为0的判断,
            if (tab == null || (n = tab.length) == 0)
// 如果为空或者长度为0,进行数组初始化,执行 initTable 方法
// 下面4.3 单独会说到 initTable 初始化方法
                tab = initTable();
// 执行到这里,说明数组不为空,计算待加入的元素应该存放的位置是否为空,
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 进到 if 里面,说明要添加的位置为空,但为了避免线程添加冲突
// 使用 CAS自旋操作,因为有可能别的线程也正在此处添加元素,
// 要保证线程的安全性,不能冲突,如果有两个线程,只有一个会添加成功,另一个会添加失败
// 另一个线程添加失败,就会重新执行判断,此时此处不为空,就会向下执行判断
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
// 其中一个线程添加成功,break退出循环,完成添加操作
                    break;                   // no lock when adding to empty bin
            }
// 这里做判断,如果为 true,说明数组正在进行扩容,然后协助其他线程完成扩容操作
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
// 如果上面都不是,说明数组既没有扩容,也不是空数组,而且要插入的位置已经有元素
// 就遍历链表中的每个节点或者树中的每个节点
            else {
                V oldVal = null;
// 此处锁的是链表的的节点,或者是树的根节点,锁粒度小,提高了并发能力
                synchronized (f) {
// 这里需要再次做一下判断,
//多线程情况下,可能其他线程添加完数据后可能会恰好链表转化成了树,或者红黑树根节点发生了旋转
// 因此要多做一步判断很有必要
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
// 这里的binCount记录的是链表的长度,若链表长度大于8可能会链表转化为树
                            binCount = 1;
// 从这里开始,遍历链表,将链表中的每一个元素与待插入的元素做比较
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
// 对链表中其他节点的key做是否相同的判断
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
//若有相同则将老的元素进行替换,并赋值给 oldVal return返回
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
// break 退出循环,添加操作结束
                                    break;
                                }
                                Node<K,V> pred = e;
// 对当前节点的下一个节点做判空操作
                                if ((e = e.next) == null) {
// 满足下一个节点为空,则将新前节点插入在当前节点的下方
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
// 插入操作完成,break 退出循环。
                                    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;
                            }
                        }
                    }
                }
// 这里的 binCount 就是数据插入完成之后的链表的长度
// 然后对数组中链表的长度做一个判断,先判断是否为0
// 不为0,则数值为插入后链表的长度,再判断是否大于等于8
// 如果满足链表的长度大于等于8,还要在 treeifyBin 方法中进一步判断数组的长度是否大于等于 64
// 如果满足数组长度大于等于64并且链表的长度大于等于8,链表会转化成红黑树,这里就不展开了
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
// 这里的 oldVal 就是被替换掉的老的元素,听名字也能看出来
// 对 oldVal 做判空操作,如果为空,则表示数组中之前没有添加过当前元素
// 如果不为空,将这个老的被替换掉的元素的值返回
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
// 这里会对数组的长度做一个维护,保证多线程下数组长度的安全性,
// 下面第专门讲到,
        addCount(1L, binCount);
        return null;
    }

3.3 initTable 初始化方法解读

ConcurrentHashMap 在调用 put 方法添加第一个元素的时候,底层就会去做初始化,在上面 putVal 放啊中也有做简单说明,初始化数组执行的就是下面这个方法,各位同学可以简单看一看该方法中的每一步,我都做了注释

private final Node<K,V>[] initTable() {
// 创建数组对象 tab,定义变量 sc;
        Node<K,V>[] tab; int sc;
// 对数组做判空操作,长度判断是否为0
        while ((tab = table) == null || tab.length == 0) {
// 这里将sizeCtl变量的值赋值给sc判断是否小于0,
            if ((sc = sizeCtl) < 0)
// 若小于0表明数组正在扩容或正在进行初始化,调用 Thread.yield 方法,
// 让线程释放CPU资源,一直得到一直释放,做自旋操作,直到其他的线程初始化完成
                Thread.yield(); // lost initialization race; just spin
// 做判断,判断 sc 和 sizeCtl是否是相等的,
// 如果相等,把 sizeCtl赋值为 -1,说明去进行初始化数组了
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
// 再次对数组做判空操作,因为有可能之前有现成已经进行初始化,这里在此作判断,防止重复初始化
                    if ((tab = table) == null || tab.length == 0) {
// 对sc做判断,sc如果大于0,取我们算出来的sc,如果不大于0,赋值默认初始容量
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
// new 了一个新的数组,长度为刚才得到的n
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 将 new 出来的数组nt赋值给 tab 再赋值给底层数组 table
                        table = tab = nt;
// 加算出数组长度的0.75倍并赋值给sc,下次sc达到这个值,就会进行扩容
                        sc = n - (n >>> 2);
                    }
                } finally {
// 将计算出来的 sc 赋值给 sizeCtl
                    sizeCtl = sc;
                }
// 退出循环
                break;
            }
        }
// 返回得到的数组 tab
        return tab;
    }

4.4 put 添加元素方法总结概括

通过上面的了解,我们大致可以知道 ConcurrentHashMap 在进行put操作添加元素时是什么样的一个过程,我大致总结了以下几点

(1)ConcurrentHashMap 在进行put 操作时,若数组采用无参构造创建,在 put 第一个元素时会先进行扩容,默认容量为16;

(2)ConcurrentHashMap 在进行 put 操作时,采用了 CAS自旋,循环,锁每个链表头节点数根节点的方式保证了添加元素时的线程安全性;

(3)添加元素时,ConcurrentHashMap 锁锁的是每个链表的头节点或者是树的根节点,它只是锁了当前的哈希桶,对其它元素添加到其他哈希桶的操作并没有任何影响,打个比方就是你要添加的数据位于哈希值为1的地方时,它只会锁住哈希值为1处的桶,不会锁住其他哈希值的桶位,它不像 HashTable 那样将整个数组锁起来,这样极大地提高了操作元素的效率;

(4)在添加元素完成之后,数组会去做一个判断,若数组的长度大于64并且链表的长度8时,会把链表进行树化,提高数据的查找效率,这一点与 HashMap 树化的操作类似;

(5)判断完是否需要树化的操作之后,还会判断添加的元素是否已经存在,如果存在会把原来的元素的Value值覆盖为新添加的元素的Value值,并返回被覆盖的Value值。

(5)做完上面的步骤之后,最后调用addCount 方法对数组的长度进行维护。

5. ConcurrentHashMap 树化操作何时进行?

在上面 putVal 方法中,犹如下一步判断,binCount  代表插入插入元素之后链表的长度,这里如果 binCount 大于等于8,就会执行 treeifyBin 方法

在下面 treeifyBin 方法中,记住一点即可。在链表长度大于等于8之后,还要满足数组的长度大于等于64,链表才会转化成红黑树。

6. ConcurrentHashMap 数组长度维护解读

刚才在分析 ConcurrentHashMap 的 put 操作的时候,可以看到,进行完 put 操作之后,会调用一个 addCount() 方法,这个方法就是对数组的长度做一个维护。

当多个线程同时来做插入操作时,数组长度的维护也会出现线程安全问题,我先来说一下原因,刚才上面提到了,再插入元素的时候。我们利用自旋+锁住链表头节点的方式保证线程安全。但是在添加完成数据之后,有可能不同的桶位同时添加完成要对数组长度做++操作,此时就会出现线程安全问题。

其实本身多个线程对数组长度做++的操作也可以同样利用自旋来完成,但是在多线程的情况下,采用自旋的方式效率仍然还是低一些,但是为了提高效率,它采用了另外一种做法;在 ConcurrentHashMap 内部,它还维护了另外一个普通数组,如下图所示的 CounterCell 数组,

我给大家说一下这种做法的原理。

假设现在有三个线程都完成了数据添加操作,同时要对数组长度做++操作,那么肯定会线程冲突,利用CAS自旋三个线程会去竞争谁先去++操作。假设第一个线程先执行了++操作,那么第二个线程和第三个线程都会做另外一个操作。它们会先获取各自线程的随机值,然后通过特殊的计算方法的得出一个数值,该数值对应着上面的 CounterCell 数组的位置,完成计算步骤的出对应数值之后,第二个第三个线程就会分别去 CounterCell 数组中各自对应的做++操作,如果添加成功,就算完成数组长度的维护操作。如果有第三个线程和第二个线程需要在同一个位置的 value 做++操作,产生了冲突,此时才会再去采用自旋的方式让其中一个线程重新获取新的线程随机值,再重新计算该往数组中的哪个位置的 value 做++操作。

7. ConcurrentHashMap 键和值能否为空?

这算是一个小的细节面试题;

如下为 putVal 源码的一部分,在这里可以看到,在put 元素之前,它会先对 key 和 value 做非空判断,只要有一个是控制,就会爆出空指针异常,所以 ConcurrentHashMap 是不能存控制的。

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

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

相关文章

14 - 多线程之锁优化(下):使用乐观锁优化并行操作

前两讲讨论了 Synchronized 和 Lock 实现的同步锁机制&#xff0c;这两种同步锁都属于悲观锁&#xff0c;是保护线程安全最直观的方式。 我们知道悲观锁在高并发的场景下&#xff0c;激烈的锁竞争会造成线程阻塞&#xff0c;大量阻塞线程会导致系统的上下文切换&#xff0c;增…

springBoot-使用idea创建项目添加依赖并实现数据查询

一、使用idea创建springBoot项目 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://mave…

Python小知识 - Python装饰器

Python装饰器 在Python中&#xff0c;装饰器是一个特殊的函数&#xff0c;可以将其他函数包装在装饰器函数中&#xff0c;并且将被包装的函数作为参数传递给装饰器函数。 使用装饰器的好处是可以自动在被包装的函数前后执行一些额外的代码&#xff0c;比如在函数执行前后打印日…

Linux之防火墙

目录 什么是防火墙 分类&#xff1a; Netfilter(数据包过滤) 防火墙无法完成的任务 iptables 与 firewalld 区别 iptables iptables执行原则 规则链 概念 分析 流程图 规则链分类 iptables 流量处理动作 iptables表 四种规则表 安装iptables 预处理 管理命令 …

SpringBoot整合RabbitMQ图文过程以及RabbitTemplate常用API介绍

&#x1f9d1;‍&#x1f4bb;作者名称&#xff1a;DaenCode &#x1f3a4;作者简介&#xff1a;啥技术都喜欢捣鼓捣鼓&#xff0c;喜欢分享技术、经验、生活。 &#x1f60e;人生感悟&#xff1a;尝尽人生百味&#xff0c;方知世间冷暖。 &#x1f4d6;所属专栏&#xff1a;Sp…

GUI知识点总结(二)(java)

文章目录 &#x1f412;个人主页&#x1f3c5;JavaSE系列专栏&#x1f4d6;前言&#xff1a;&#x1f993;事件 &#x1f3e8;Adapter 适配器&#x1f415;对话框&#x1f98d;showMessageDialog()&#xff1a;消息对话框&#x1f98d;showConfirmDialog()&#xff1a;确认对话…

ChatGPT 插件 “Consensus“ 实现论文搜索功能;数据工程在语言建模中的重要性

&#x1f989; AI新闻 &#x1f680; ChatGPT 插件 “Consensus” 实现论文搜索功能 摘要&#xff1a;OpenAI 推出了一个名为 “Consensus” 的插件&#xff0c;可在 ChatGPT 上进行论文搜索。用户只需用一句话描述自己想了解的问题&#xff0c;插件就能从 2 亿篇论文中搜索并…

Tomcat架构设计源码剖析

Tomcat架构设计&源码剖析 Tomcat 架构设计 Tomcat的功能&#xff08;需求&#xff09; 浏览器发给服务端的是一个 HTTP 格式的请求&#xff0c;HTTP 服务器收到这个请求后&#xff0c;需要调用服务端程序来处理&#xff0c;所谓的服务端程序就是你写的 Java 类&#xff…

实现 js 中所有对象的深拷贝(包装对象,Date 对象,正则对象)

通过递归可以简单实现对象的深拷贝&#xff0c;但是这种方法不管是 ES6 还是 ES5 实现&#xff0c;都有同样的缺陷&#xff0c;就是只能实现特定的 object 的深度复制&#xff08;比如数组和函数&#xff09;&#xff0c;不能实现包装对象 Number&#xff0c;String &#xff0…

如何压缩图片大小?缩小图片体积跟我学

在日常生活中&#xff0c;我们常常需要处理图片&#xff0c;但是由于图片大小过大&#xff0c;常常带来许多不便。那么&#xff0c;如何压缩图片大小呢&#xff1f;下面就为大家介绍三个方法&#xff0c;让你轻松解决这个问题。 一、使用图片编辑软件 市面上有许多图片编辑软件…

使用HTTP代理上网安全吗?

HTTP代理是一种代理服务器&#xff0c;它可以充当客户端和服务器之间的中介&#xff0c;以帮助客户端访问服务器上的资源。虽然使用HTTP代理可以带来一些便利&#xff0c;但是在安全方面也存在一些问题。 HTTP代理的安全问题 窃取用户信息 如果HTTP代理服务器不受信任&#xff…

【计算机网络】https协议

目录 概念的准备 什么是加密 为什么需要加密 常见的加密方式 对称加密 非对称加密 数据摘要(数字指纹) 数字签名 https的工作过程 方案一&#xff1a;只使用对称加密 方案二&#xff1a;只使用非对称加密 方案三&#xff1a;双方都采用非对称加密 方案四&#xff…

【Spring Boot 源码学习】深入 FilteringSpringBootCondition

走近 AutoConfigurationImportFilter 引言往期内容主要内容1. match 方法2. ClassNameFilter 枚举类3. filter 方法 总结 引言 前两篇博文笔者带大家从源码深入了解了 Spring Boot 的自动装配流程&#xff0c;其中自动配置过滤的实现由于篇幅限制&#xff0c;还未深入分析。 …

2023国赛数学建模C题思路模型 - 蔬菜类商品的自动定价与补货决策

# 1 赛题 在生鲜商超中&#xff0c;一般蔬菜类商品的保鲜期都比较短&#xff0c;且品相随销售时间的增加而变差&#xff0c; 大部分品种如当日未售出&#xff0c;隔日就无法再售。因此&#xff0c; 商超通常会根据各商品的历史销售和需 求情况每天进行补货。 由于商超销售的蔬菜…

uniapp使用webview将页面转换成图片支持h5、app、小程序

uniapp使用webview将页面转换成图片支持h5、app、小程序 在uniapp项目中新建主页和webview页面 index.vue代码 <template><view><!-- 微信小程序要设置src为网络路径 --><web-view src"/hybrid/html/webview.html"></web-view><…

十四、内置模块path、邂逅Webpack和打包过程、css-loader

一、内置模块path &#xff08;1&#xff09;path介绍 &#xff08;2&#xff09; path常见的API 这里重点讲一下path.resolve()。 看上面的例子&#xff0c;从右往左开始解析&#xff0c;所以一开始解析的就是 /abc.txt &#xff0c;这个时候就会把它当成一个绝对路径了&am…

C#,数值计算——用于积分函数与方法的Stiel类的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { public class Stiel { public class pp : UniVarRealValueFun, RealValueFun { public Stiel st { get; set; } null; public pp() { } public doubl…

探索数据库管理的利器 - PHPMyAdmin

有一个项目&#xff0c;后端由博主独自负责&#xff0c;最近需要将项目交接给另一位同事。在项目初期&#xff0c;博主直接在数据库中使用工具创建了相关表格&#xff0c;并在完成后利用PhpMyAdmin生成了一份数据字典&#xff0c;供团队使用。然而&#xff0c;在随后的开发过程…

JDK1.8下载、安装和环境配置使用

JDK1.8下载、安装和配置 下载安装包解压文件配置测试安装 下载安装包 链接地址 https://pan.baidu.com/s/1RF7-ulq0_qAelpXskDxdvA 提取码 d1y0解压文件 jdk1.8.0_181 配置 右击我的电脑&#xff0c;选择属性 2.点击高级系统设置 在系统变量区里点击&#xff1a;新建…

听书网站模板源码 懒人书院网站源码 苹果cms手机听书网站模版源码 支持手机端

苹果cms超漂亮UI高仿芒果TV听书网站模板带手机端。 手机版修改logo&#xff0c;ting_wap/images/logo.png 电脑版修改logo&#xff0c;ting_pc/img/logo.png 编辑推荐后台推荐5颗星。 新势力/热播榜单后台推荐9颗星。