【从零开始学习JAVA集合 | 第一篇】深入解读HashMap源码(含面试题)

目录

目录

前言:

HashMap简介:

HashMap的常用常量和变量: 

HashMap的重要考点: 

HashMap的存储过程:

HashMap的扩容过程:

HashMap的初始化:

常见面试题:

总结:


前言:

HashMap 是 Java 中最常用的数据结构之一,它提供了快速的查找、插入和删除操作,可以满足大多数情况下的需求。然而,要深入理解 HashMap 的内部实现和工作原理并不是一件容易的事情。本篇文章旨在深入解读 HashMap,包括其内部结构、工作原理、常见的使用场景以及性能优化等方面。

通过本文的阅读,读者将能够更好地掌握 HashMap 的工作原理,了解其在实际项目中的应用,并能够在需要时根据特定需求进行合理的选择和优化。希望本文能够成为读者学习和掌握 HashMap 的良好起点,为大家在日常编程中应用 HashMap 提供指导和帮助。

HashMap简介:

HashMap 是 Java 中的一个常用数据结构,用于存储键值对(key-value pairs)。它基于哈希表(hash table)实现,允许 null 键和 null 值,并且具有快速的查找、插入和删除操作。HashMap 继承自抽象类 AbstractMap,并且实现了 Map 接口。

这里有一个有趣的知识点:HashMap既继承了,又实现了Map接口。但是明明AbstractMap就已经实现了Map接口,为什么HashMap不直接使用父类AbstractMap的Map接口呢?

据Java集合框架创始人所说:自己在写这段逻辑代码的时候,错误的使用了这种方式,但当时以为  不让HashMap直接继承父类的接口可能在以后会有用,而直到现在这种方式也没有表现出什么作用,而目前的JDK维护者认为其是一个无关紧要的问题,因此也就没有对这里进行修改。

HashMap 的内部结构主要由数组和链表。它通过计算键的哈希值来决定存储位置,使用链表法来解决哈希冲突。当链表的长度大于等于8并且数组的长度大于64的时候,链表就会转为红黑树

当链表的长度等于大于8,但是数组的长度小于64的时候,此时会选择对数组进行扩容而不是将链表转化为红黑树,这是因为在数据量比较小的情况下,使用红黑树反而会降低查询效率。

我们来总结一下HashMap的特点:

  • 存取无序
  • 键和值的位置都可以是null,但是键的位置只能是一个null
  • 键的位置是唯一的,底层的数据结构控制键
  • JDK1.8之前的数据结构是:链表+数组,JDK 1.8后是链表+数组+红黑树
  • 链表长度大于8并且数组长度大于64,才会将链表转化为红黑树,变为红黑树的目的是为了高效的查询
  • HashMap是线程不安全的,但由于整个类都是无锁结构,因此效率比较高

HashMap的常用常量和变量: 

1.序列化版本号常量:

    private static final long serialVersionUID = 362498820763181265L;

 2.集合的初始化容量(必须是2的n次幂):

   static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

3.负载因子常量:

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

4.链表转红黑树时最小链表长度常量:

    static final int TREEIFY_THRESHOLD = 8;

5.链表转红黑树时最小数组长度常量:

    static final int MIN_TREEIFY_CAPACITY = 64;

6.红黑树转为链表的最小数组长度常量

    static final int UNTREEIFY_THRESHOLD = 6;

6.数组阈值(调整下一次扩容的容量值)(容量 * 负载因子)

    int threshold;

7.负载因子变量:

    final float loadFactor;

8.存储元素的数组:

    transient Node<K,V>[] table;

 9.存放具体元素的集合:

    transient Set<Map.Entry<K,V>> entrySet;

10.实际存储元素的个数:

    transient int size;

11. 记录HashMap的修改次数:

    transient int modCount;

HashMap的重要考点: 

HashMap的存储过程:

JDK1.8之前,HashMap由数组+链表 数据结构组成。

JDK1.8以后,HashMap由数组+链表+红黑树 数据结构组成。

而本文我们只讲讲解基于数组+链表+红黑树的HashMap。我们接下来用一个实例来讲解HashMap的存储过程。

public class Demo01 {
    public static void main(String[] args) {


        HashMap hashMap = new HashMap<String, Integer>();
        hashMap.put("a", 1);
        hashMap.put("b", 2);
        hashMap.put("c", 3);

        System.out.println(hashMap);
    }
}

当我们执行存储操作的时候,会发生如下操作:

  1. 计算哈希值:当调用put方法时,HashMap会将传入的key通过哈希函数(hash function)转换为一个整数,这个整数被称为哈希值。
  2. 计算数组索引:HashMap使用哈希值对数组的长度进行取模运算,得到一个数组索引(即数组的位置),用于确定键值对在数组中的存储位置。
  3. 存储链表或红黑树:如果该位置上已经有其他键值对存在,那么新的键值对将会以链表或红黑树的形式插入到该位置的末尾或中间。如果该位置还没有任何键值对,那么直接将键值对存储到该位置。

需要注意的是:在JDK8之前,HashMap的构造方法就会帮我们创建一个长度为16Entry[] table 用来存储键值数据。在JDK8以后就不是在HashMap的构造方法底层来创建数组了,而是在第一次调用put方法的时候创建一个Node[] table 来存储键值对数据。

好的,我们现在已经基本了解了HashMap是怎么插入一个数据的,接下来让我们看一下put方法的源码:

我们转到putVal中查看一下:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

小插入: 

我们对Key调用了哈希算法得到了一个int类型的整数,而这个哈希算法的底层逻辑是采用Key的HashCode方法的值结合数组长度进行无符号右移(>>),按位异或(^)

这个计算方式相对于直接使用hashCode()方法的好处在于,可以减少哈希冲突的概率。通过将二进制位的高位和低位都参与到哈希值的计算中,可以更好地保持哈希值的随机性和分布性,从而减少不同键的哈希冲突,提高HashMap的性能。

其实我们还有不少的方法同样可以得到一个hash值,比如取余。但位运算是硬件操作,可以直接在CPU的寄存器中进行操作,效率要高得多。因此我们一般都会使用位运算代替取余。而用位运算代替算术运算在后面还很常见,我们在这里要有这样的认识。

并且通过这个方法我们也可以看出:HashMap的Key是可以为null的。当Key为null的时候,将位置0作为键值对的插入位置。 

现在让我们现在正式的开始解析这段代码:

  • 首先,代码将table赋值给变量tab,并进行判空操作。如果table为空,则说明哈希表尚未初始化,此时将n设为0。
  • 如果table不为空,则将table的长度赋值给变量n。
  • 接下来,通过条件运算符判断n是否为0,如果是0,则表明哈希表的长度为0,需要进行扩容操作。在这种情况下,代码会调用resize()方法进行哈希表的扩容,并将扩容后的哈希表赋值给tab,并将扩容后的哈希表的长度赋值给n。
  • 最后,返回n作为哈希表的长度。

总的来说,这段代码的作用是获取哈希表的长度,并在需要扩容时进行相应的处理。通过检查哈希表的长度是否为0,可以判断哈希表是否需要进行初始化或者扩容,从而保证HashMap的正常使用和性能优化。

  • 首先,代码计算出要插入的位置i,通过对hash值进行(n-1) & hash的位与运算得到。这里的n是哈希表的长度,通常是2的幂,通过这个位与运算可以将hash值限定在0到n-1之间,确保落在哈希表范围内。
  • 然后,代码判断tab[i]位置是否为空,即当前位置是否已经存在节点。如果为空,则将新节点插入到这个位置。
  • 如果tab[i]位置不为空,则说明当前位置已经存在节点。这里可能会涉及到链表或者红黑树等数据结构,用于处理哈希冲突的情况,但在这段代码中没有展现出来。

总的来说,这段代码的作用是根据hash值找到对应的位置,然后判断该位置是否已经存在节点,如果不存在则直接插入新节点,如果存在则根据具体的情况进行相应的处理,以保证HashMap中的键值对能够正确存储和检索。

  • 首先,代码定义了一个Node和一个K类型的变量k。然后通过一系列的条件判断来确定如何处理哈希冲突:
    • 首先,判断当前节点p的哈希值和键与要插入的哈希值和键是否相等,如果相等,则将当前节点p赋值给e。
    • 如果不相等,且当前节点p是TreeNode类型,则调用TreeNode的putTreeVal方法来处理插入。
    • 如果以上两个条件都不满足,则通过遍历链表的方式找到合适的位置插入新节点,同时处理可能出现的树化操作。

总的来说,这段代码的作用是处理哈希冲突的情况,具体处理方式取决于当前节点的类型以及与要插入节点的哈希值和键的比较结果。通过合适的处理方式,保证HashMap在处理哈希冲突时能够正确地插入新节点,并维护数据结构的完整性。

  • 首先,将已存在节点e的值赋给oldValue,以便在后面返回旧的值。

  • 然后会根据onlyIfAbsent的取值来判断是否需要覆盖已存在节点e的值:

    • 如果onlyIfAbsent为false,或者oldValue为null(即允许替换已有的空值),则将新值value赋给节点e的值e.value。
  • 接着调用afterNodeAccess(e)方法来进行相关操作,比如LinkedHashMap中重写的方法会将节点移动到链表末尾,以实现LRU策略。

  • 最后返回旧的值oldValue,表示已存在键对应的旧值。

总的来说,这段代码的作用是在HashMap中处理键已存在的情况,根据需要更新节点的值,并在操作后返回旧的值。

  • 首先,对modCount进行自增操作,用于在并发情况下对HashMap的修改进行控制。

  • 然后对size进行自增操作,表示当前HashMap中键值对的数量增加了1。接着会检查size是否超过了阈值threshold,如果超过了则需要进行哈希表的扩容操作。

  • 如果size超过了threshold,就调用resize方法对哈希表进行扩容。哈希表的扩容会重新计算每个键值对的位置,以降低哈希冲突的概率。

  • 接着调用afterNodeInsertion(evict)方法来进行相关操作,其中evict参数表示是否需要进行LRU策略的节点移除操作。

  • 最后返回null,表示插入操作完成并且不需要返回任何值。

总的来说,这段代码的作用是在HashMap中处理插入新节点后更新相关计数器、进行哈希表的扩容操作,并在操作后进行相关处理。

 所以其实整个put操作的代码逻辑链其实是比较清晰的,我们可以用图表示为:

HashMap的扩容过程:

之所以要讲HashMap的扩容过程,是因为他的扩容和我们想的有点不一样,我们先看代码:

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

按照我们的传统思路来讲:当原数组进行扩容之后,当我们想要把原数组中的数据放到新数组中的时候,基本的思路是对原数据进行重新Hash计算位置,放入数组。但这样的效率太低了。HashMap的扩容并没有采用这种机制。

我们先来看源代码中HashMap的扩容是如何做的:

简而言之:HashMap认为:旧节点的新位置要么在原位置(newTab[j] = loHead),要么在原位置+旧容量(newTab[j + oldCap] = hiHead)上 

 我们来看一下为什么可以这么认为:

当我们进行扩容的时候,每次的大小都是翻倍的(2的n次幂),例如从4到8,16到32。这种内存翻倍后与原来计算(n-1)& hash 的结果相比,只是多了一个bit位。

例如:当我们从4 扩容的 8 的时候:

​​​n:

4:0100        16:010000

--------------------------------------------------------------------------------------------------------------------------

8:1000        32:100000

n-1

3:0011        15:001111

--------------------------------------------------------------------------------------------------------------------------

8:0111        32:011111

也就是说实际上扩容后,从二进制来看,运算结果只有最高位发生了变化,如果元素的二进制最高位为0,那么就是原位置,如果二进制最高是1,那么就会变为1,也就是原位置+旧容量

正是因为这种巧妙的rehash的方式,大大提高了运行效率。

HashMap的初始化:

我们不讲HashMap的自动初始化,源码比较简单,大家可以自己看一看。而在这里要着重讲一下HashMap的手动扩容过程:

我们都知道:HashMap的扩容过程是一个比较浪费时间的过程。因此我们如果想要提高代码运行的效率,就要手动设置初始大小,避免自动扩容。但有的人会在这里陷入一个误区,我们通过一个实际问题来引出:

当我们要存放七个元素的时候,我们应该手动初始化大小为多少呢?

 很多同学肯定下意识的回答 8 。 但其实是错误的!让我们来思考一下:

如果设置为8 的话, 负载系数默认是0.75。那么 8 * 0.75 =6,也就是说数组中实际元素达到6个就要扩容,我们如果存放七个元素,手动设置大小为8 的话,还是避免不了扩容。 

因此:我们应该 手动设置为(需要存储的元素个数/负载因子)+1。这是计算手动设置容量的通用方法。

注意:不可以修改负载系数,0.75是官方大量数据统计出来的一个比较均衡的负载因子,我们基本不会对其做修改!

常见面试题:

1.为什么集合的初始化容量必须是2的n次幂?

首先,当我们尝试向HashMap中添加一个元素的时候,需要根据Key的hash值得到数组中的具体位置。而我们不可以直接使用Key的hash值。这是因为Key的hash值不一定就在数组下标范围内,因此我们要对Key的hash值再次进行操作,使其满足值的范围在数组下标范围内

这里的第一个思路就是取余。我们让hash%n(数组长度),这样就可以控制得到的值始终在数组下标范围内。

但是这里又有一个问题:如果是取余的话,效率是很低的,不如使用位运算。而我们的代码在实际中也是用位运算来代替取余操作

而我们在实际中也是这么做的,但是hash % n 和 (n - 1)& hash 的值相等的前提是 n 是2 的 x 次幂

 而除此之外,返回到使用这个方法的最外层,我们的目的还是为了让数据能够均匀分布,减少Hash冲突。

如果创建HashMap的时候,输入的数组长度不是2的幂,那么HashMap就会通过位运算得到一个比我们输入的数组长度大的,离我们输入数组长度最近的一个2的幂。

2.hash方法为什么要右移16位异或?((h = key.hashCode()) ^ (h >>> 16))

其中 h = key.hashCode() ^ (h >>> 16) 的目的是为了增加哈希值的随机性,使得节点在哈希表中分布更均匀,减少哈希冲突,提高查找效率。

具体来说:

  • 首先,通过 key.hashCode() 获取键的哈希码。
  • 接着,通过 h >>> 16 将 h 右移16位,然后将结果与 h 进行异或操作 ^。这样做是为了让高位的信息参与到低位的计算中,增加低位的随机性。

通过这样的运算,可以让原始的哈希值的高位和低位进行混合,并且引入了一定程度的随机性,使得最终的哈希值分布更加均匀,减少了哈希冲突的概率。这种处理方式有助于提高HashMap的性能,特别是在处理大量数据时,能够更有效地分散数据,减少链表长度,提高查找效率。

如果当哈希值的高位变化很大,低位变化很小,这样就很容易造成哈希冲突了,所以这里把高低位都利用起来,从而解决了这个问题。

3.负载因子的作用是什么

负载因子用来判断当前数组的负载程度,简单来讲就是有多少元素。当数组中的实际存在元素个数大于 数组长度 * 负载因子的 时候,就会进行扩容操作。 这点我们可以在put代码中看到:

我们可以看到当数组size大于threshold的时候,就进行扩容操作。我们可以在resize方法中看到thresould的计算方法:

 4.HashMap如何保证初始化数组长度始终是2的n次方的

 我们可以看到默认的带参构造函数对我们输入的数据进行了这样的操作,具体方法如下:

 static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

而这个方法中又调用了 Integer.numberOfLeadingZeros 方法,我们来看看:

 numberOfLeadingZeros方法用来计算32位长度的二进制数字的前导零:

例如 9 的二进制:

00000000 00000000 00000000 00001001

那么9的前导零就是28,我们来看看代码执行结果:

好的,让我们回到  tableSizeFor   方法中 :

 static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

我们来模拟一下tableSizeFor操作:

  1. 输入一个数字 5。
  2. numberOfLeadingZeros方法 对(5-1)得到结果为:29。
  3. n等于-1>>>29,结果为7。
  4. 进入判断: n>0 则执行(n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1。
  5. n< MAXIMUM_CAPACITY(1<<30),则返回n+1,也就是7+1 =7

这样,我们就得到了一个比5大的,距离5最小的一个二次幂。这就是tablesizeFor的运行原理。

需要注意的是这里返回的结果并没有直接赋值给HashMap的初始实例数组长度,而是给了扩容阈值 threshould !!!‘

而真正的初始化实际上发生在第一次调用put方法插入元素的时候:

总体的逻辑为:当旧数组为空,旧阈值不为空的时候,把旧阈值赋值给新数组。 

就这样,我们就实现了带参数的HashMap实例创建。

5.HashMap将链表转化为红黑树的链表边界长度为什么是8

这个问题其实在HashMap的源码中就给了解释:

简要的意思就是:由于树节点的大小是链表节点的两倍,因此我们轻易不会将链表转化为红黑树,这会对内存造成浪费。而在大量的实际数据插入的情况下,我们统计发现:箱子中的节点的频率服从泊松分布。

当连续插到链表的第八个节点的时候,实际上概率已经很小了,已经达到了0.00000006。也就是说我们将链表的长度设置为8,就已经可以应对大多数的HashMap的Hash碰撞了 。当节点大于8的时候,我们再考虑查询的效率问题,将其转化为红黑树。

总结来讲,将链表转换红黑树的链表长度边界设置为8,是综合时间和空间的结果。

总结:

综上所述,本文深入介绍了HashMap的底层源码实现原理。首先,我们深入探讨了HashMap的数据结构,它基于数组和链表(或红黑树)实现,这使得HashMap能够高效地存储键值对。其次,我们详细分析了HashMap的哈希算法,通过hashCode方法和位运算来确定元素在数组中的位置,实现了快速的查找、插入和删除操作。对于扩容与负载因子的讨论使我们更好地理解了HashMap是如何动态调整大小以保持性能的。

通过本文的介绍,读者不仅可以深入了解HashMap的具体实现细节,还能够从中学习到在实际开发中如何更好地利用HashMap的特性。深入理解HashMap的底层源码将有助于提升对Java集合框架的整体理解,并能够在日常开发中更加灵活、高效地应用HashMap这一重要的数据结构。

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux 高级管理,MySQL服务器的构建与维护

实验环境 某公司因业务范围日益扩大&#xff0c;最近订购了一套基于B/S架构的电子商务系统&#xff0c;在正式部署之前&#xff0c;要 求对现有的httpd服务器进行改造&#xff0c;首先需要增加MySQL数据库服务。 需求描述 1. 编译安装MySQL服务器&#xff0c;并添加为mysqld系…

mybatisplus saveBatch版本问题导致CPU打满生产问题定位

一、生产现象 1、16:57 运维告知Push微服务有一台因为CPU被打满&#xff0c;自动重启&#xff0c;询问原因。 2、17:00 查看异常节点CPU轨迹,16:30开始CPU出现异常飙升 3、17:10 结合生产日志错误&#xff0c;以及定时任务运行情况&#xff0c;得出结论&#xff1a; 产品在…

conda的安装及使用 以pycharm 为例

下载 https://docs.conda.io/en/latest/miniconda.html 下载 window版本 74M且下着吧。 安装 一路next或agree &#xff0c;不同意人家也不会按装 。重要的是安装目录 让andconda当老大 pycharm的使用 创建项目时如下图选择 成功后进入项目的Terminal则如下图表示成功

【Pytorch】学习记录分享1——Tensor张量初始化与基本操作

1. 基础资料汇总 资料汇总 pytroch中文版本教程 PyTorch入门教程 B站强推&#xff01;2023公认最通俗易懂的【PyTorch】教程&#xff0c;200集付费课程&#xff08;附代码&#xff09;人工智能_机器 视频 1.PyTorch简介 2.PyTorch环境搭建 basic: python numpy pandas pytroch…

Ubuntu22.04 LTS + CUDA12.3 + CUDNN8.9.7 + PyTorch2.1.1

简介 本文记录Ubuntu22.04长期支持版系统下的CUDA驱动和cuDNN神经网络加速库的安装&#xff0c;并安装PyTorch2.1.1来测试是否安装成功。 安装Ubuntu系统 如果是旧的不支持UEFI启动的主板&#xff0c;请参考本人博客U盘系统盘制作与系统安装&#xff08;详细图解&#xff09…

03 Temporal 详细介绍

前言 在后端开发中&#xff0c;大家是否有遇到如下类型的开发场景 需要处理较多的异步事件需要的外部服务可靠性较低需要记录保存某个对象的复杂状态 在以往的开发过程中&#xff0c;可能更多的直接使用数据库、定时任务、消息队列等作为基础&#xff0c;来解决上面的问题。然…

入门PostgreSQL:安装和设置数据库的完整指南!

下载和安装 PostgreSQL&#xff1a; 访问 PostgreSQL 的官方网站(https://www.postgresql.org/)并下载适合你操作系统的最新版本。 执行安装程序&#xff0c;并按照提示完成安装过程。 在安装过程中&#xff0c;你需要设置超级用户(Superuser)密码&#xff0c;这是用于管理数…

【C++】仿函数在模板中的应用——【默认模板实参】详解(n)

前言 大家好吖&#xff0c;欢迎来到 YY 滴C系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; 目录 一.引入&#xff1a;查看(容器)文档时常…

开发案例:使用 canvas 实现图表系列之折线图

一、功能结构 实现一个公共组件的时候&#xff0c;首先分析一下大概的实现结构以及开发思路&#xff0c;方便我们少走弯路&#xff0c;也可以使组件更加容易拓展&#xff0c;维护性更强。然后我会把功能逐个拆开来讲&#xff0c;这样大家才能学习到更详细的内容。下面简单阐述…

C语言好题分享七(三数之和)

❀❀❀ 文章由不准备秃的大伟原创 ❀❀❀ ♪♪♪ 若有转载&#xff0c;请联系博主哦~ ♪♪♪ ❤❤❤ 致力学好编程的宝藏博主&#xff0c;代码兴国&#xff01;❤❤❤ 三数之和 题目来源LeetCode&#xff1a;刷题传送门 题目&#xff1a;给你一个整数数组 nums &#xff0c;判断…

Linux——MySQL数据库的使用

访问MySQL数据库 MySOL数据库系统也是一个典型的C/S&#xff08;客户端/服务器&#xff09;架构的应用&#xff0c;要访问MySOL数据库 需要使用专门的客户端软件&#xff0c;在Linux系统中&#xff0c;最简单、易用的MySQL.客户端软件是其自带的mysql 命令工具。 登录到MySQL服…

Vue3-13- 【v-for】循环一个对象

说明 v-for 这个东西就很神奇&#xff0c;可以遍历一个对象&#xff0c; 当然&#xff0c;它遍历对象是通过 对象的属性名&#xff0c;遍历对象的属性值。语法格式如下 &#xff1a; v-for"(value,key,index) in objName" value : 属性的值 key &#xff1a;属性的k…

商品规格的实现

在商城项目中购买商品或者添加购物车的时候都会让我们去选择商品的规格,颜色、尺码、风格等,这里把刚做完的此功能代码记录下,方便以后查阅: <template><view><u-navbar title="测试"></u-navbar><view class="content"&g…

多篇整合版:最全电商erp系统接口测试实战

之前我们讲了电商ERP系统接口简介以及如何使用post方式获取接口请求 &#xff0c;今天我们来讲解如何用JMeter实现接口功能、性能测试。 内容&#xff1a; JMeter实现接口功能测试 JMeter实现接口的性能测试 JMeter实现接口功能测试 企业性能测试编写脚本过程&#xff1a;接口…

java学生选课系统 数据库版

首先让我们创建一个数据库 让我们向表中插入数据然后查询它

WSL 配置 Docker 内存和 CPU 资源限制

我用的电脑一共有40G内存&#xff0c;最近发现电脑重启后&#xff0c;VmmemWSL 进程很快就会占用一多半的内存&#xff08;20G&#xff09;&#xff0c;电脑中有多个停止运行的容器&#xff0c;正常启动状态的只有一个 MySQL 服务&#xff0c;通过 docker stats 查看占用内存也…

【详解优先级队列(堆)】

目录 堆的概念 堆的性质 堆的存储方式 堆的创建 堆的向下调整 向下过程(以小堆为例) 向下过程(以大堆为例) 建堆的时间复杂度O(n) 堆的插入与删除 堆的插入 向上调整建堆的时间复杂度O(nlogn) 堆的删除 常见习题 常用接口介绍 PriorityQueue的特性 Pri…

实战1-python爬取安全客新闻

一般步骤&#xff1a;确定网站--搭建关系--发送请求--接受响应--筛选数据--保存本地 1.拿到网站首先要查看我们要爬取的目录是否被允许 一般网站都会议/robots.txt目录&#xff0c;告诉你哪些地址可爬&#xff0c;哪些不可爬&#xff0c;以安全客为例子 2. 首先测试在不登录的…

使用MIB builder自定义物联网网关的MIB结构

文章目录 物联网网关初识&#xff08;了解即可&#xff09;IoT的通用MIB库结构MIB Builder开发流程指导问题总结子叶没所属分组值范围不为0 物联网网关初识&#xff08;了解即可&#xff09; 网关又称网间连接器、协议转换器。简单说&#xff0c;物联网网关是一台智能计算机&a…

【Java】网络编程-UDP回响服务器客户端简单代码编写

这一篇文章我们将讲述网络编程中UDP服务器客户端的编程代码 1、前置知识 UDP协议全称是用户数据报协议&#xff0c;在网络中它与TCP协议一样用于处理数据包&#xff0c;是一种无连接的协议。 UDP的特点有&#xff1a;无连接、尽最大努力交付、面向报文、没有拥塞控制 本文讲…