HashMapConcurrentHashMap

news2024/11/23 21:34:26

文章目录

    • 1、HashMap基础
      • 类属性
      • node
      • 容量
      • 负载因子
      • hash算法
    • 2、数组+链表/树
      • 为什么引入链表
      • 为什么jdk1.8会引入红黑树
      • 为什么一开始不就使用红黑树?
      • HashMap的底层数组取值的时候,为什么不用取模,而是&
      • 数组的长度为什么是2的次幂
      • 如果指定数组的长度不为 2次幂,就破坏了数组的长度是2次幂的这个规则吗?
    • 3、HashMap源码
      • 构造方法
      • put()方法
      • get()方法
      • HashMap扩容原理
        • jdk1.7
        • jdk1.8
        • 经典面试题
      • HashMap不安全的原因
      • 常用方法
    • 4、ConcurrentHashMap
      • 1.7
      • 1.8
      • 总结

img

null作为key只能有一个,作为value可以有多个

1、HashMap基础

类属性

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 序列号
    private static final long serialVersionUID = 362498820763181265L;
    // 默认的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认的负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当桶(bucket)上的结点数大于等于这个值时会转成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    // 当桶(bucket)上的结点数小于等于这个值时树转链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中结构转化为红黑树对应的table的最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存储元素的数组,总是2的幂次倍
    transient Node<k,v>[] table;
    // 存放具体元素的集
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的个数,注意这个不等于数组的长度。
    transient int size;
    // 每次扩容和更改map结构的计数器
    transient int modCount;
    // 阈值(容量*负载因子) 当实际大小超过阈值时,会进行扩容
    int threshold;
    // 负载因子
    final float loadFactor;
}

node

HashMap的内部类 Node<K, V> ,实现了Entry接口

// 属性
final int hash;
final K key;
V value;
Node<K,V> next; 

next:记录下一个Node节点,通过next可顺序遍历链表所有节点

容量

默认16

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

负载因子

默认0.75

static final float DEFAULT_LOAD_FACTOR = 0.75f;

扩容触发:元素个数 > 容量 * 负载因子,扩容

hash算法

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

1、先获取key的hashCode值——h

2、h与h右移16位做异或运算——低16位与高16位做异或运算,高16位参与hash,减少冲突

2、数组+链表/树

为什么引入链表

HashMap底层是数组,当进行put操作时,会进行hash计算,判断元素位置。当多个元素在同一个数组位置时,会引起hash冲突,因此引入链表,解决hash冲突

为什么jdk1.8会引入红黑树

当链表长度大于8时,遍历查找效率较慢,故引入红黑树

链表长度>8,且数组长度>64,才变成红黑树

为什么一开始不就使用红黑树?

红黑树相对于链表维护成本大,红黑树在插入新数据之后,可能会通过左旋、右旋、变色来保持平衡,造成维护成本过高,故链路较短时,不适合用红黑树

HashMap的底层数组取值的时候,为什么不用取模,而是&

i = (n - 1) & hash

计算机运算时,&比取模运算快

数组的长度为什么是2的次幂

1、减少hash冲突

数据均匀分布,可以减少hash冲突,所以使用hashCode%size可以最大程度的平均分配。当n为2的次幂时,(n-1)&hash=hash%n

2、&运算速度比%快,Java中快10倍左右

3、保证索引值在capacity中不会超出数组长度

如果指定数组的长度不为 2次幂,就破坏了数组的长度是2次幂的这个规则吗?

不会的,HashMap 的tableSizeFor方法做了处理,能保证n永远都是2次幂

/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
    //cap-1后,n的二进制最右一位肯定和cap的最右一位不同,即一个为0,一个为1,例如cap=17(00010001),n=cap-1=16(00010000)
    int n = cap - 1;
    //n = (00010000 | 00001000) = 00011000
    n |= n >>> 1;
    //n = (00011000 | 00000110) = 00011110
    n |= n >>> 2;
    //n = (00011110 | 00000001) = 00011111
    n |= n >>> 4;
    //n = (00011111 | 00000000) = 00011111
    n |= n >>> 8;
    //n = (00011111 | 00000000) = 00011111
    n |= n >>> 16;
    //n = 00011111 = 31
    //n = 31 + 1 = 32, 即最终的cap = 32 = 2 的 (n=5)次方
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

3、HashMap源码

构造方法

// 默认构造函数
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
 }

 // 包含另一个“Map”的构造函数
 public HashMap(Map<? extends K, ? extends V> m) {
     this.loadFactor = DEFAULT_LOAD_FACTOR;
     putMapEntries(m, false);
 }

 // 指定“容量大小”的构造函数
 public HashMap(int initialCapacity) {
     this(initialCapacity, DEFAULT_LOAD_FACTOR);
 }

 // 指定“容量大小”和“负载因子”的构造函数
 public HashMap(int initialCapacity, float loadFactor) {
     if (initialCapacity < 0)
         throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
     if (initialCapacity > MAXIMUM_CAPACITY)
         initialCapacity = MAXIMUM_CAPACITY;
     if (loadFactor <= 0 || Float.isNaN(loadFactor))
         throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
     this.loadFactor = loadFactor;
     // 初始容量暂时存放到 threshold ,在resize中再赋值给 newCap 进行table初始化
     this.threshold = tableSizeFor(initialCapacity);
 }

四个构造方法中,都初始化了负载因子 loadFactor。HashMap中没有 capacity字段,初始化容量initialCapacity 是通过tableSizeFor 将其扩容到与 initialCapacity 最接近的2的幂次方大小,然后暂时赋值给 threshold ,后续通过 resize 方法将 threshold 赋值给 newCap 进行 table 的初始化

put、get是jdk1.8时的情况

put()方法

1、key是null,直接插入

2、key不为null,判断key是否与put(key)相同,相同返回老key

3、key不为null,如果索引位置的值是树结构,调用putTreeVal()添加数据

4、key不为null,如果索引位置的值是链表结构,遍历链表,key相同时返回值;遍历到尾部也不同,尾插法插入数据

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果table为空,或者还没有元素时,则扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 如果首结点值为空,则创建一个新的首结点。
    // 注意:(n - 1) & hash才是真正的hash值,也就是存储在table位置的index。在1.6中是封装成indexFor函数。
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {    // 到这儿了,就说明碰撞了,那么就要开始处理碰撞。
            Node<K,V> e; K k;
            // 如果在首结点与我们待插入的元素有相同的hash和key值,则先记录。
            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);
                            // 当遍历的结点数目大于8时,则采取树化结构。
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                                break;
                        }
                        // 如果找到与我们待插入的元素具有相同的hash和key值的结点,则停止遍历。此时e已经记录了该结点
                        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;
                // onlyIfAbsent表示如果当前位置已存在一个值,是否替换,false是替换,true是不替换
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);  // 这个是空函数,可以由用户根据需要覆盖
                return oldValue;
            }
        }
    ++modCount;
    // 当结点数+1大于threshold时,则进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict); // 这个是空函数,可以由用户根据需要覆盖
    return null;
}

HashMap在put()的时候,如果put一个已经存在的key,那么会把老的key对应的value值返回

public static void main(String[] args) {
    HashMap<Integer, Integer> map = new HashMap<>();
    map.put(1,5);
    Integer put = map.put(1, 8);
    System.out.println(put); // 5
}

get()方法

1、获取key对应的数组位置 key?=key.hashCode()

2、节点为树节点,调用getTreeNode();不为树节点(链表节点),循环遍历链表查值

3、都没查到,返回null

 public V get(Object key) {
        //定义一个Node对象来接收
        Node<K,V> e;
        //调用getNode()方法,返回值赋值给e,如果取得的值为null,就返回null,否则就返回Node对象e的value值
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

 //取hash值方法,HashMap的put方法的也是调用了这个方法,get方法也调用这个方法,保证存取时key值对应的hash值是一致的,这样才能正确对应 
 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    
     
final Node<K,V> getNode(int hash, Object key) {
        //定义几个变量 
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //首先是判断数组table不能为空且长度要大于0,同时把数组长度tab.length赋值给n
        if ((tab = table) != null && (n = tab.length) > 0 &&
             //其次是通过[(n - 1) & hash]获取key对应的索引,同时数组中的这个索引要有值,然后赋值给first变量
            (first = tab[(n - 1) & hash]) != null) {
            //这个first其实就是链表头的节点了,接下来判断first的hash值是否等于传进来key的hash值
            if (first.hash == hash && 
                //再判断first的key值赋值给k变量,然后判断其是否等于key值,或者判断key不为null时,key和k变量的equals比较结果是否相等
                ((k = first.key) == key || (key != null && key.equals(k))))
                //如果满足上述条件的话,说明要找的就是first节点,直接返回
                return first;
            //走到这步,就说明要找的节点不是首节点,那就用first.next找它的后继节点 ,并赋值给e变量,在这个变量不为空时   
            if ((e = first.next) != null) {
                //如果首节点是树类型的,那么直接调用getTreeNode()方法去树里找
                if (first instanceof TreeNode)
                     //这里就不跟进去了,获取树中对应key的节点后直接返回
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //走到这步说明结构还是链表    
                do {
                    //这一步其实就是在链表中遍历节点,找到和传进来key相符合的节点,然后返回
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                  //获取e节点的后继节点,然后赋值给e,不为空则进入循环体  
                } while ((e = e.next) != null);
            }
        }
        //以上条件都不满足,说明没有该key对应的数据节点,返回null
        return null;
    }

HashMap扩容原理

jdk1.7

什么时候扩容?扩容多少?

重要成员变量:

  • size:map中包含的Entry数量
  • threshold:扩容的阈值,threshold = loadFactor * capacity
  • capacity:桶的长度

1、当map中包含的Entry的数量大于等于threshold = loadFactor * capacity的时【size>=threshold】,且新建的Entry刚好落在一个非空的桶上,此刻触发扩容机制,将其容量扩大为2倍

2、当size大于等于threshold的时候,并不一定会触发扩容机制,但是会很可能就触发扩容机制,只要有一个新建的Entry出现哈希冲突,则立刻resize

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
    
      	// 当size大于等于某一个阈值thresholdde时候且该桶并不是一个空桶;
        resize(2 * table.length);//将容量扩容为原来的2倍
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);//扩容后的,该hash值对应的新的桶位置
    }
    
    createEntry(hash, key, value, bucketIndex);//在指定的桶位置上,创建一个新的Entry
}

扩容过程

rezise大致思想:先计算新的容量和threshold,在创建一个新hash表,最后将旧hash表中元素rehash到新的hash表中

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
		//最大容量为 1 << 30
        threshold = Integer.MAX_VALUE;
        return;
    }

    Entry[] newTable = new Entry[newCapacity];//新建一个新表
    boolean oldAltHashing = useAltHashing;
    useAltHashing |= sun.misc.VM.isBooted() &&
            (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean rehash = oldAltHashing ^ useAltHashing;//是否再hash
    transfer(newTable, rehash);//完成旧表到新表的转移
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
		//遍历同桶数组中的每一个桶
        while(null != e) {
			//顺序遍历某个桶的外挂链表
            Entry<K,V> next = e.next;//引用next
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);//找到新表的桶位置;原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不同的桶中去了,有效的缓解了哈希冲突。
            e.next = newTable[i];//头插法插入新表中
            newTable[i] = e;
            e = next;
        }
    }
}

多线程下,transfer缺乏同步机制,可能出现大量线程对新桶数组进行transfer,造成死循环、数据丢失,因此HashMap不安全。

jdk1.8

相比于1.7,resize做了很大改进

1、参数:oldCap——原table长度,newCap——新table长度,newCap=2*oldCap

2、4个变量:loHead、loTail、hiHead、hiTail,用于计算table下标的新算法

  • 正常情况:hash&(oldTable.length-1)
  • 扩容之后:hash&(newTable.length-1)=hash&(2*oldTable.length-1)

e.hash & oldCap:用于计算位置b到底是0还是1用的,只要其结果是0,则新散列下标=原散列下标,否则新散列坐标要在原散列坐标的基础上加上原table长度

  • loHead,下标不变情况下的链表头
  • loTail,下标不变情况下的链表尾
  • hiHead,下标改变情况下的链表头
  • hiTail,下标改变情况下的链表尾

(e.hash & oldCap) == 0:代表散列下标不变的情况下,只使用了loHead和loTail两个参数,由他们组成了一个链表,否则将使用hiHead和hiTail参数

所以,只要loTail不是null,说明链表中的元素在新table中的下标没变,所以新table的对应下标中放的是loHead,另外把loTail的next设为null

hiTail不是null,说明链表中的元素在新table中的下标,应该是原下标加原table长度,新table对应下标处放的是hiHead,另外把hiTail的next设为null

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)                      //注释1
            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) {                                 //注释2
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)                                        //注释3
                    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) {                      //注释4
                            if (loTail == null)                            //注释5
                                loHead = e;
                            else
                                loTail.next = e;                           //注释6
                            loTail = e;                                    //注释7
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {                                  /注释8
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
经典面试题

1、计划用HashMap存1k条数据,构造时传1000会触发扩容吗

HashMap 初始容量指定为 1000,会被 tableSizeFor() 调整为 1024;但是它只是表示 table 数组为 1024;负载因子是0.75,扩容阈值会在 resize() 中调整为 768(1024 * 0.75)

会触发扩容,如果需要存储1k的数据,应该传入1000 / 0.75(1333)。tableSizeFor() 方法调整到 2048,不会触发扩容

2、计划用HashMap存1w条数据,构造时传10000会触发扩容吗

当我们构造HashMap时,参数传入进来 1w,经过 tableSizeFor() 方法处理之后,就会变成 2 的 14 次幂 16384负载因子是 0.75f,可存储的数据容量是 12288(16384 * 0.75f)。完全够用,不会触发扩容

HashMap不安全的原因

1、多线程put并发的时候可能造成数据的丢失

2、多线程put和get并发的时候,可能造成get为null

(线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题)

jdk1.7中HashMap因为头插入,导致get时出现死循环;jdk1.8使用尾插法解决了死循环,但是还是会造成节点丢失问题

并发场景下,使用ConcurrentHashMap

常用方法

获取所有key:map.keySet()

获取所有value:map.values()

获取key对应的value:map.get(key)

遍历,同时获取key、value:

entrys = map.entrySet()

entry.getKey()

entry.getValue()

其他:

get、size、isEmpty、remove、replace、containsKey、containsValue

参考:https://blog.csdn.net/weixin_43689480/article/details/118752906

https://javaguide.cn/java/collection/hashmap-source-code.html

4、ConcurrentHashMap

详情请学习:https://javaguide.cn/java/collection/concurrent-hash-map-source-code.html

下面是一些知识点总结

1.7

1、存储结构

ConcurrentHashMap是由多个segment组合,每个segment内部是一个HashMap,HashMap内可进行扩容,但是segment初始化后不能更改,默认16个segment,也就是默认支持最多 16 个线程并发

segment数组+HashMap(HashEntry数组)+链表

2、初始化

// 无参构造
// initialCapacity-16默认初始化容量
// loadFactor-0.75f默认负载因子
// concurrencyLevel-16默认并发级别
public ConcurrentHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

// 有参构造
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {
}

有参构造:

  1. 必要参数校验
  2. 校验并发级别 concurrencyLevel 大小,如果大于最大值,重置为最大值。无参构造默认值是 16.
  3. 寻找并发级别 concurrencyLevel 之上最近的 2 的幂次方值,作为初始化容量大小,默认是 16。
  4. 记录 segmentShift 偏移量,这个值为【容量 = 2 的 N 次方】中的 N,在后面 Put 时计算位置时会用到。默认是 32 - sshift = 28.
  5. 记录 segmentMask,默认是 ssize - 1 = 16 -1 = 15.
  6. 初始化 segments[0],默认大小为 2,负载因子 0.75,扩容阀值是 2*0.75=1.5,插入第二个值时才会进行扩容

3、put

  1. 计算要 put 的 key 的位置,获取指定位置的 Segment
  2. 如果指定位置的 Segment 为空,则初始化这个 Segment
  3. Segment.put 插入 key,value 值

初始化segment:

  • 检查计算得到的位置的 Segment 是否为 null.
  • 为 null 继续初始化,使用 Segment[0] 的容量和负载因子创建一个 HashEntry 数组
  • 再次检查计算得到的指定位置的 Segment 是否为 null.
  • 使用创建的 HashEntry 数组初始化这个 Segment.
  • 自旋判断计算得到的指定位置的 Segment 是否为 null,使用 CAS 在这个位置赋值为 Segment

Segment 继承了 ReentrantLock**,所以** Segment 内部可以很方便的获取锁

scanAndLockForPut:不断自旋tryLock()获取锁;自旋次数>指定次数,lock()阻塞获取锁

4、rehash扩容

扩容到原来的2倍,要么位置不变,要么变为index+oldSize,扩容后使用链表头插法插入元素

5、get

  1. 计算得到 key 的存放位置
  2. 遍历指定位置查找相同 key 的 value 值

1.8

1、存储结构

Node数组+链表/红黑树

2、初始化

初始化是通过自旋和 CAS 操作完成的

变量:sizeCtl当前初始化状态

3、put

  1. 根据 key 计算出 hashcode
  2. 判断是否需要进行初始化
  3. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功
  4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容
  5. 如果都不满足,则利用 synchronized 锁写入数据
  6. 如果数量大于 TREEIFY_THRESHOLD 则要执行树化方法,在 treeifyBin 中会首先判断当前数组长度 ≥64 时才会将链表转换为红黑树

4、get

  1. 根据 hash 值计算位置
  2. 查找到指定位置,如果头节点就是要找的,直接返回它的 value
  3. 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之
  4. 如果是链表,遍历查找之

总结

img

img

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

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

相关文章

数据结构--》数组和广义表:从基础到应用的全面剖析

数据结构为我们提供了组织和处理数据的基本工具。而在这个广袤的数据结构领域中&#xff0c;数组和广义表是两个不可或缺的重要概念。它们作为线性结构的代表&#xff0c;在算法与应用中扮演着重要的角色。 无论你是初学者还是进阶者&#xff0c;本文将为你提供简单易懂、实用可…

青少年近视问题不容小觑,蔡司用专业技术助力孩子视力健康发展

根据国家卫健委公布的数据显示&#xff0c;2022年全国儿童青少年近视率达到53.6%&#xff0c;青少年近视已成为社会普遍的眼健康问题。对家长来说&#xff0c;也需要提高对孩子眼视光健康重要性的认知&#xff0c;日常培养青少年良好的用眼习惯&#xff0c;并通过矫正视力的方式…

如何使用 Tensor.art 实现文生图

摘要&#xff1a;Tensor.art 是一个基于 AI 的文本生成图像工具。本文介绍了如何使用 Tensor.art 来实现文生图的功能。 正文&#xff1a; 文生图是指将文本转换为图像的技术。它具有广泛的应用&#xff0c;例如在广告、教育和娱乐等领域。 Tensor.art 是一个基于 AI 的文本…

外汇天眼:真实记录,投资者在盗版MT4平台SCE Group上做交易的经历!

外汇市场是全球最大的金融市场&#xff0c;比起其他市场有着更多天然的优势&#xff0c;但也因为资讯的不对等&#xff0c;导致很多人上当受骗。而在外汇市场上最常见的骗局之一&#xff0c;就是黑平台使用盗版MT4/5交易软件&#xff0c;因为截至目前MT4/5仍是外汇市场交易使用…

汽车电子中的安森美深力科分享一款高性能车规级芯片NCV7520MWTXG

安森美深力科NCV7520MWTXG可编程六沟道低压侧 MOSFET 预驱动器&#xff0c;是一个 FLEXMOS™ 汽车级产品系列&#xff0c;用于驱动逻辑电平 MOSFET。该产品可通过串行 SPI 和并行输入组合控制。该器件提供可兼容 3.3 V/5 V 的输入&#xff0c;串行输出驱动器可基于 3.3 V 或 5 …

在模拟器上安装magisk实现Charles抓https包(三)

经过前两篇的内容&#xff0c;链接如下&#xff1a; 在模拟器上安装magisk实现Charles抓https包&#xff08;一&#xff09;_小小爬虾的博客-CSDN博客 在模拟器上安装magisk实现Charles抓https包&#xff08;二&#xff09;_小小爬虾的博客-CSDN博客 电脑端的Charles就可以抓…

VS2022 17.8 功能更新:现已支持 C11 线程

早在 VS2022 17.5 版本&#xff0c;Microsoft Visual C 库已经初步支持了 C11 atomics。今天&#xff0c;我们很高兴地宣布&#xff0c;在最新版本 VS2022 17.8 预览版 2 中已正式支持 C11 线程。开发者可以更轻松地将跨平台 C 应用程序移植到 Windows&#xff0c;而无需开发线…

华为OD机试 - 最小步骤数(Java 2023 B卷 100分)

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路五、Java算法源码六、效果展示1、输入&#xff1a;4 8 7 5 2 3 6 4 8 12、输出&#xff1a;23、说明&#xff1a;4、思路分析 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《…

网络安全总结

前言 本文内容主要摘抄网络规划设计师的教材和腾讯-SUMMER课堂&#xff0c;主要对网络安全进行简单梳理和总结 OSI安全体系 X轴表示8种安全机制&#xff0c;Y轴表示OSI7层模型&#xff0c;Z轴表示5种安全服务&#xff0c;图中X是水平&#xff0c;Y轴竖直&#xff0c;Z轴向外…

2023年中国喷头受益于技术创新,功能不断提升[图]

喷头行业是一个专注于生产和供应各种类型喷头的产业。喷头是一种用于将液体、气体或粉末等物质喷射或喷洒的装置&#xff0c;广泛应用于不同领域&#xff0c;包括工业、农业、家用、医疗等。 喷头行业分类 资料来源&#xff1a;共研产业咨询&#xff08;共研网&#xff09; 随…

Redis 获取、设置配置文件

以Ubuntu 为例 redis配置文件 cd /etc/redis sudo vim redis.conf 获取配置文件、修改配置文件

【轻松玩转MacOS】网络连接篇

引言 本篇让我们来聊聊网络连接。不论你是在家、在办公室&#xff0c;还是咖啡厅、机场&#xff0c;几乎所有的MacOS用户都需要连接到互联网。在这个部分&#xff0c;我们将向你展示如何连接到互联网和局域网。让我们开始吧&#xff01; 一、连接到互联网 首先&#xff0c;我…

农业育种好策略:凌恩生物种质资源数字化全方位解决方案

动植物育种是通过创造遗传变异、改良遗传特性&#xff0c;以培育具有优良性状的动植物新品种的技术。随着高通量组学技术的发展和应用&#xff0c;分子育种等现代科学理论与技术得以发展和不断完善&#xff0c;是未来作物育种的不二选择&#xff0c;它的精准性、高效性都将带领…

NoSQL之 Redis命令工具及常用命令

目录 1 Redis 命令工具 1.1 redis-cli 命令行工具 1.2 redis-benchmark 测试工具 2 Redis 数据库常用命令 2.1 set&#xff1a;存放数据&#xff0c;命令格式为 set key value 2.2 get&#xff1a;获取数据&#xff0c;命令格式为 get key 2.3 keys 命令可以取符合规则的…

深入探索地理空间查询:如何优雅地在MySQL、PostgreSQL及Redis中实现精准的地理数据存储与检索技巧

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

入门级气传导耳机推荐哪款?安利几款好用的气传导耳机

​在当今的快节奏生活中&#xff0c;音乐成为了我们放松身心的重要方式。然而&#xff0c;我们在享受音乐的同时&#xff0c;也面临着耳机线缆的束缚和耳朵的压迫感。这时&#xff0c;气传导耳机应运而生&#xff0c;它们以一种更加先进、舒适的方式来传递音乐&#xff0c;为我…

【C++】-C++11中的知识点(上)--右值引用,列表初始化,声明

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树&#x1f388; &#x1f389;作者宣言&#xff1a;认真写好每一篇博客&#x1f4a4; &#x1f38a;作者gitee:gitee✨ &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法&#x1f384; 如 果 你 …

Linux基本指令一

Linux基本指令一 一、ls指令1、语法2、功能3、常用选项4、示例 二、pwd指令1、功能2、示例 三、cd指令1、语法2、功能3、常用操作4、示例 四、 touch指令1、语法2、功能3、示例 五、mkdir指令1、语法2、功能3、常用选项4、示例 六、rmdir指令1、语法2、适用对象3、功能4、常用选…

孙哥Spring源码第27集

第27集、手写实现AOP 【视频来源于&#xff1a;B站up主孙帅suns Spring源码视频】【微信号&#xff1a;suns45】 1、手写实现AOP的缺点有哪些&#xff1f; 增加额外功能时&#xff0c;会对所有的方法 都加入对应的功能 问题 不灵活 &#xff08;事务 CUD 加入 R 不应该加入&a…

聊聊JDK19特性之虚拟线程 | 京东云技术团队

1.前言 在读《深入理解JVM虚拟机》这本书前两章的时候整理了JDK从1.0到最新版本发展史&#xff0c;其中记录了JDK这么多年来演进过程中的一些趣闻及引人注目的一些特性&#xff0c;在调研JDK19新增特性的时候了解到了虚拟线程这个概念&#xff0c;于是对虚拟线程进行学习整理内…