您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

搞懂hashMap底层原理

时间:2023-08-03 12:41:46  来源:微信公众号  作者:码农本农

说明

hashMap在JAVA1.7和java1.8版本中有做一些调整,我们本篇只说java1.7的hashMap。

数据结构

搞懂hashMap底层原理

hashMap的数据结构是由数组和链表组成,table是一个存放Entry对象的数组,每个Entry对象由4个属性组成,分别是key、value、next、hash,key和value是我们熟知的键值对,不需要过多解释,next是当前元素在链表中指向下一个元素的引用,hash是计算出来的hashcode,hashMap中的hsah是通过对key.hashcode()进行一定操作得出的,并不是直接使用key.hashcode()方法计算数来的值。

属性信息

先来了解下hashMap中一些重要的属性

//Hashmap的初始化大小,初始化的值为16,1往右移4位为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 

//HashMap是动态扩容的,就是容量大小不能大于 1<<30
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认的扩容因子,这个值可以通过构造修改
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//空数据,默认构造的时候赋值为空的Entry数组,在添加元素的时候
//会判断table=EMPTY_TABLE ,然后就扩容
static final Entry<?,?>[] EMPTY_TABLE = {};

//表示一个空的Hashmap
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

//Hashmap的大小
transient int size;

//threshold表示当HashMap的size大于threshold时会执行resize操作。
DEFAULT_INITIAL_CAPACITY=16

//扩容的阈值
int threshold;

//扩容因子,没有传入就使用默认的DEFAULT_LOAD_FACTOR = 0.75f
final float loadFactor;

//数据操作次数,用于迭代检查修改异常
transient int modCount;

static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

put方法

步骤:

  1. 判断table是否为空,如果为空则初始化,不为空则下一步
  2. 判断key是否为null,如果为null则处理,不为null则下一步
  3. 根据key计算下标
  4. 如果下标处的桶不为空,则从下标处开始遍历链表,如果找到key相同的则更新覆盖并返回旧值,如果为空则下一步
  5. 插入前判断是否需要扩容,如果需要则进行扩容,如果不需要就下一步
  6. 头插法插入桶中

接下来我们对每一步骤分别展开讨论

1.table初始化

private void inflateTable(int toSize) {
    // 找到大于等于指定数组长度的2的n次方
    int capacity = roundUpToPowerOf2(toSize);
 // 扩容阈值,数组长度*负载因子
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    // 创建数组
    table = new Entry[capacity];
    // 是否重新赋值hashSeed 
    initHashSeedAsNeeded(capacity);
}

private static int roundUpToPowerOf2(int number) {
    // 使用Integer的highestOneBit方法找到大于等于指定数组长度的2的n次方
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}


数组初始化其实就三个步骤:计算数组容量,创建数组,判断是否重hash

「数组容量」: 如果指定数组长度值大于MAXIMUM_CAPACITY(最大数组容量:2的30次方),就用最大值;如果指定数组长度值小于等于1,就用1;如果指定数组长度值大于1,就用下面的方法得到大于等于指定数组长度的第一个2的n次方的值。

Integer.highestOneBit((number - 1) << 1)

public static int highestOneBit(int i) {
        // HD, Figure 3-1
        i |= (i >>  1);
        i |= (i >>  2);
        i |= (i >>  4);
        i |= (i >>  8);
        i |= (i >> 16);
        return i - (i >>> 1);
    }

这个方法内部是通过位移和亦或操作得到的值,感兴趣的可以直接看下源码。

「创建数组」:直接用计算出来的数组长度创建Entry数组table,元素类型为Entry。

「判断是否重hash」

重hash就是对同个key重新计算hash值,那么为什么要重新计算hash值,实际上只是为了让hsah值更复杂,在计算下标的时候就会更加散列,减少hash冲突。

那么什么样的条件下才会重hash呢,从源码可以看出switching为true表示需要重hash,影响switching取值的是下面这段代码

final boolean initHashSeedAsNeeded(int capacity) {
 // hashSeed 初始值为0,false
    boolean currentAltHashing = hashSeed != 0;
    // 数组长度是否 >= 2^31-1 
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    // 使用异或,currentAltHashing 为false,只有数组长度>= 2^31-1时返回true
    boolean switching = currentAltHashing ^ useAltHashing;
    //switching为true,则hashSeed重新赋值(一般不会出现)
    if (switching) {
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
    return switching;
}

initHashSeedAsNeeded方法用来判断是否进行重hash,如果需要重hash,会同步更新hash种子,最后返回boolean类型的值。

sun.misc.VM.isBooted()指jvm运行状态,一般为true;

hashSeed初始为0,所以currentAltHashing一定为false;

Holder.ALTERNATIVE_HASHING_THRESHOLD取的是环境变量jdk.map.althashing.threshold配置的值(程序员配置),如果没有配置就默认取Integer.MAX_VALUE。

通过上面的分析可以知道是否进行重hash只会受到capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD的影响。

「我们可以得出一个结论:」如果程序员不配置环境变量jdk.map.althashing.threshold,那么就永远不会进行重hash,因为数组长度capacity最大是2的30次方,而Integer.MAX_VALUE的值是2的31次方减1,即这个条件永远不会满足,但是你可能会说扩容的时候传入的capacity正好是最大值2的30次方的2倍,但是我会告诉你,如果数组成都达到2的30次方,是不允许扩容的。

所以说如果程序员不设置环境变量,initHashSeedAsNeeded方法实际上没有意义的。

那为什么要更新hash种子呢?

这就要了解下hsah值是怎么生成的了:

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
 
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

hashMap的hsah值是由key调用自身的hashCode方法得到的值与hashSeed进行5次亦或操作4次位移运算得到的。

所以相同的key只有在hashSeed变化后才有生成不同的hash值,否则生成的永远是相同的hsah值,所以需要重hash的时候就必须改变hashSeed,否则重hash的结果会和原来一样。

2.处理key为null

private V putForNullKey(V value) {
 // 当key为null时,数组下标指定为0
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
     // 判断现在有没有key为null的Entry
        if (e.key == null) {
         //value替换为新值,返回旧的value
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    //修改次数+1
    modCount++;
    // table[0]中没有值或没有匹配到null的key,创建一个Entry放入table[0]
    addEntry(0, null, value, 0);
    return null;
}

上面这段代码是对key为null的情况的处理,同时也说明hashmap是允许key为null的,通过上面可以看出hashMap中key为null的元素只会存储在数组下标为0的桶中,如果桶中有多个,就遍历链表找到key为null的元素进行覆盖更新,如果桶中无元素,就调用addEntry方法插入元素,这里只需要知道调用addEntry方法的结果是将数据插入数组下标为0的桶中,addEntry方法我们下面再具体看。

3.计算下标

// 获取key的hash值
    int hash = hash(key);
    // 根据hash值和数组长度计算要放入的数组下标位置
    int i = indexFor(hash, table.length);

计算下标实际上分为两步,计算hsah值和计算下标,计算下标的原理是用hash值除以数组容量,得到的余数就是下标,用这种方式可以保证不同的key一定会放在数组中的某个桶中,不会越界,而hash值可以让不同的key在数组中的分部足够分散,减少hsah冲突。

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
 
    h ^= k.hashCode();
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

上面已将说过hash方法,这里再来说一次,我们知道java中每个类都默认由hashcode方法生成hashcode,但是hashMap中并没有直接用此方法生成的hashcode,而是对其生成的hahshcode与hash种子进行亦或和位移操作,1.7的hashMap在计算hsah的时候进行了5次亦或操作和4次位移运算,这样做的目的就是使得不同的key计算出来的hash更加分散,更加能减少hash冲突。

static int indexFor(int h, int length) {
    // 计算数组下标位置,数组长度必须为2的n次方
    return h & (length-1);
}

我们上面说了hash除以数组容量得到数组下标,但是这种做法在java中太慢了,而和此做法同效的一种方式就是h & (length-1),就是数组容量减去1,再与hash做&操作。这种方式在java中是非常高效的。

4.遍历查找key

运行到这里,数组已经有了,key对应的下标也有了,接下来就是插入操作,插入之前会先查看下标对应的桶中是否为空桶,如果不为空桶就要先遍历查找是否有相同key。

for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        // 判断key的值是否相等
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
         // key的值相等则写入新的value值,将旧value返回
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    
   // 修改次数+1
    modCount++;
    // 下标位置为空或没有能够匹配的key值,创建Entry放入链表
    addEntry(hash, key, value, i);

因为hashMap是由数组和链表组成,数组的每个桶中都由链表组成,所以需要遍历链表查找相同的key,如果存在相同的key就更新覆盖,如果没有,就调用addEntry方法进行插入。

//addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
 // 如果当前数组长度>=扩容阈值 并且 当前数组下标位置不为null
    if ((size >= threshold) && (null != table[bucketIndex])) {
     // 数组扩容为当前长度*2
        resize(2 * table.length);
        // key是否为null,不为null计算hash值,null则直接hash值为0
        hash = (null != key) ? hash(key) : 0;
        // 根据hash值和新数组长度计算下标位置
        bucketIndex = indexFor(hash, table.length);
    }
 //创建Entry放入table中
    createEntry(hash, key, value, bucketIndex);
}

可以看到在正式添加元素之前会先判断是否需要扩容,如果需要则先进行扩容。

5.扩容处理

从上面的源码可以知道扩容需要满足两个条件:

  • 数组长度达到threshold,threshold是由负载因子和数组容量计算出来的

  • 当前数组下标对应的桶不为null

如果满足条件则进入resize方法进行扩容

void resize(int newCapacity) {
 // 原数组
    Entry[] oldTable = table;
    // 原数组长度
    int oldCapacity = oldTable.length;
    // 如果原数组长度为2^30,不进行扩容,把扩容阈值修改为2^31-1
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
 // 根据新的数组长度,创建数组
    Entry[] newTable = new Entry[newCapacity];
    // 转移数组数据 
    // 调用initHashSeedAsNeeded方法,根据新数组的长度判断是否会hashSeed重新赋值
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    // table指向新数组
    table = newTable;
    // 计算新的扩容阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

// transfer方法
void transfer(Entry[] newTable, boolean rehash) {
 // 新数组长度
    int newCapacity = newTable.length;
    // 遍历原数组的Entry
    for (Entry<K,V> e : table) {
     // Entry不为null
        while(null != e) {
         // 先找到链表中下一个要转移的Entry
            Entry<K,V> next = e.next;
            // 如果hashSeed变了,重新进行hash计算
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            // 重新计算数组下标,结果分两种:1.与原下标一直 2.原下标+本次扩容了多长
            int i = indexFor(e.hash, newCapacity);
            /*原数组统一链表中的数据转移到新数组中时,所处链表顺序颠倒,因此多线程的情况下可能会出现环形链表问题*/
            // Entry的next指向新数组的链表头
            e.next = newTable[i];
            // Entry放入新数组的链表中
            newTable[i] = e;
            // 下一次进行操作的就是原数组链表的下一个
            e = next;
        }
    }
}

扩容步骤:

  1. 首先传入的newCapacity是数组容量的2倍,也是2的n次方
  2. 如果数组的容量已经达到2的30次方,则不进行扩容,直接返回
  3. 用新的数组长度newCapacity创建Entry数组
  4. initHashSeedAsNeeded判断是否要重hash,并更新hash种子
  5. transfer方法进行扩容处理

「transfer方法进行具体的扩容处理:」

实际上就是遍历旧数组,从旧数组的第一个桶中拿到链表,从链表头部开始遍历,一个一个的取出计算新的下标(如果需要重hash就会用新的hsah种子计算hsah值,如果不需要重hash就用原来的hsah值,最终用hsah值和新数组容量计算下标),然后头插法插到新的数组中。

「你会发现两个规律:」

  • 原来数组中的链表和新数组中链表的顺序相反。

  • 计算所得的新下标要么等于原下标值,要么等于原下标加扩容的长度。

我们通过一个例子来分析一下第二条规律。

假设table.length=16,现在有两个key,key1对应的hash值为68,key2对应hash值为84,根据公式h & (length-1)计算,&运算规则是遇0为0,结果如下:

68 key1 0100 0100 & 0000 1111  =0000 0100  =4 84 key2 0101 0100 & 0000 1111  =0000 0100  =4

可见两个值都落在table[4]这个桶中。

扩容一次后table.length=32,再根据公式h & (length-1)计算结果如下

68 key1 0100 0100 & 0001 1111  =0000 0100  =4 84 key2 0101 0100 & 0001 1111  =0001 0100  =20

可见68依然被放在新数组的table[4],而84被放在table[20]

再扩容一次后table.length=64,再根据公式h & (length-1)计算结果如下

68 key1 0100 0100 & 0011 1111  =0000 0100  =4 84 key2 0101 0100 & 0011 1111  =0001 0100  =20

可见两个值还在原来的数组下标对应的桶中。

结论:同一个桶中的链表中的数据,扩容后,在新数组中的下标要么和原数组相同,要么是原数组下标加扩容的长度。

这个规律是怎么形成的呢?

这得益于数组的容量都为2的n次方,数组每次扩容的后结果都是原来数组容量的两倍,例如:16,32,64...,length-1的结果分别是15,31,63,而对应的二进制如下:

0000 1111

0001 1111

0011 1111

可以看的出,每次扩容都是高位加1,就导致在计算的时候只需要看hash值中与高位对应的那个位上是0还是1,也就导致新数组中的下标只有两种可能:如果是0下标不变,如果是1下标就会变化。

这个规律有什么好处呢?

这个规律可以使原来在同个桶中的数据可以分散到其他桶中,使得数组分部更加均匀,减少hash冲突,扩容的时候同个桶中的数据将会被分部到新数组中的哪个桶中,可以通过只判断高位就能确定,所以可以以此来优化扩容效率。

6.头插法

// createEntry方法
void createEntry(int hash, K key, V value, int bucketIndex) {
 // 获取当前数组下标位置的链表头
    Entry<K,V> e = table[bucketIndex];
    // 在链表头位置创建新的Entry,next指向原链表头(头插法)
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    // 数组长度+1
    size++;
}

头插法的源码就很简单了,就是新建一个Entry对象,新建Entry对象的next属性指向当前坐标下的头部Entry对象,再把新的Entry对象引用赋值给当前数组下标处。

这里的文字说明可能比自己看源码更绕。自己看源码应该一目了然。

问题汇总

为什么要计算得到大于等于指定数组长度的第一个2的n次方的值

只有在数组长度为2的n次方时,数组长度-1转换为2进制时才可以转换为低位全部为1的二进制,和hash值进行&运算时才能等效于hash值除以数组容量求余的结果。

为什么要用头插法?

实际上无论是头插法还是尾插法,都是需要遍历链表的,如果在遍历过程中找到key相同的,则更新覆盖,这种情况不会有插入操作,所以无所谓头插法和尾插法,但是如果没有找到key相同的元素,那这时肯定已经遍历到链表的尾部了,所以但凡插入,不管是头插法还是尾插法都不会在遍历链表上节省时间,又因为链表的插入仅仅是更换next属性的指针,所以两种插入方式的效率是没有区别的。

java1.7之所以使用头插法应该和自身的代码结构有关,因为插入方法是独立的,如果用尾插法,就要在遍历的时候记录最后一个元素的值,而头插法就不需要了,但是我觉得这个不是主要原因,个人觉得java开发者只不过是两者选择了一个而已,没有什么特殊考虑,否则也不至于会有循环链表的问题了。

hashMap为什么是线程不安全的

hashMap线程不安全主要表现在两个方面

  • 多线程插入数据的时候,数据丢失问题
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++;
}

上面是头插法的代码逻辑,在多线程操作下,如果两个线程同时走到方法内第一行,那么获取到的e是相同的,然后两个线程分别创建Entry对象,并且Entry对象的next属性赋值为e,这样一来总会有一个线程的数据被丢掉了。

  • 多线程情况下扩容的时候可能会发生循环链表

循环链表发生在多线程扩容的情况下,下面是扩容的部分代码:

for (Entry<K,V> e : table) {
    while(null != e) {
        Entry<K,V> next = e.next;
        if (rehash) {
          e.hash = null == e.key ? 0 : hash(e.key);
        }
        int i = indexFor(e.hash, newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
    }

这段代码逻辑是把旧数组中的数据从链表头部一个个利用头插法插入到新数组中。

假设有两个线程同时进行扩容,两条线程都执行到下面这行代码:

Entry<K,V> next = e.next;

此时第一条线程继续执行,第二条线程卡住,等到第一条线程整个循环执行结束后,第二条线程才继续执行。

此时第一条线程扩容完成,链表的指向和原数组的顺序相反。假设原数组的某个桶中链表方向是1>2>3>4,扩容后又恰好都落在同一个新的桶中,那么新的链表方向是4>3>2>1。

此时第二条线程开始执行循环:

第一轮循环开始 e指的是1,next指的是2 头插法插入新的数组,新数组的链表为1>null

第二轮循环开始 e指的是2,next指的是1 头插法插入新的数组,新数组的链表为2>1>null

第三轮循环开始 e指的是1,next指的是null 头插法插入新的数组,新数组的链表为1>2>1

此时循环链表出现了。



Tags:hashMap   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Search: hashMap  点击:(11)  评论:(0)  加入收藏
HashMap:Java中的高效数据结构
HashMap是Java中常用的数据结构之一,它实现了Map接口,并且提供了快速的查找、插入和删除操作。HashMap的底层数据结构是数组和链表(或红黑树)的组合,这种数据结构被称为哈希表(Has...【详细内容】
2023-11-24  Search: hashMap  点击:(329)  评论:(0)  加入收藏
HashMap的底层数据结构
在 JDK1.8 中,HashMap 还引入了一个新的概念,叫做负载因子(load factor),它是指哈希表中键值对的数量与数组长度的比值。当键值对的数量超过了负载因子与数组长度的乘积时,就会...【详细内容】
2023-09-15  Search: hashMap  点击:(239)  评论:(0)  加入收藏
HashMap 的基础结构,必须掌握!
HashMap 是一种散列表,它存储的内容是键值对(key-value)映射。在 HashMap 中,每个键(key)映射到一个值(value)。散列表的工作原理是:当通过 put() 方法将键值对存储在 HashMap...【详细内容】
2023-09-14  Search: hashMap  点击:(277)  评论:(0)  加入收藏
HashMap 是怎么解决哈希冲突的?
前言 今天来分享一道比较好的面试题,“HashMap 是怎么解决哈希冲突的?”对于这个问题,我们一起看看考察点和比较好的回答吧!考察点 现在的企业级开发中HashMap几乎是...【详细内容】
2023-09-11  Search: hashMap  点击:(198)  评论:(0)  加入收藏
搞懂hashMap底层原理
说明hashMap在java1.7和java1.8版本中有做一些调整,我们本篇只说java1.7的hashMap。数据结构hashMap的数据结构是由数组和链表组成,table是一个存放Entry对象的数组,每个Entry...【详细内容】
2023-08-03  Search: hashMap  点击:(105)  评论:(0)  加入收藏
HashMap线程不安全体现在哪里?
HashMap线程不安全体现在哪里?如果你到现在还不清楚赶紧看下去,明明白白补一补~。在Java中,HashMap是一种常用的数据结构,它以键值对的形式存储和管理数据。然而,由于HashMap在...【详细内容】
2023-04-27  Search: hashMap  点击:(291)  评论:(0)  加入收藏
如何实现线程安全的HashMap?
要实现线程安全的 HashMap,可以考虑以下几种方法: 使用 ConcurrentHashMap:ConcurrentHashMap 是线程安全的 HashMap 实现,采用了分段锁的机制,可以提高并发性能。 使用 Collecti...【详细内容】
2023-03-21  Search: hashMap  点击:(266)  评论:(0)  加入收藏
三分钟轻松搞懂 HashMap 死循环问题!
HashMap 死循环发生在 JDK 1.7 版本中,形成死循环的原因是 HashMap 在 JDK 1.7 使用的是头插法,头插法 + 链表 + 多线程并发 + HashMap 扩容,这几个点加在一起就形成了 HashMap...【详细内容】
2023-01-31  Search: hashMap  点击:(256)  评论:(0)  加入收藏
HashMap核心原理分析
学习目标1、hash冲突的解决办法有哪几种2、HashTable、hashmap、CHM三者之间的区别3、HashMap的默认长度是多少?默认扩容因子是多少?4、HashMap它是怎么解决hash冲突的5、Hash...【详细内容】
2022-09-13  Search: hashMap  点击:(134)  评论:(0)  加入收藏
▌简易百科推荐
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(5)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(12)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(8)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(10)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(8)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
Kubernetes 究竟有没有 LTS?
从一个有趣的问题引出很多人都在关注的 Kubernetes LTS 的问题。有趣的问题2019 年,一个名为 apiserver LoopbackClient Server cert expired after 1 year[1] 的 issue 中提...【详细内容】
2024-03-15  云原生散修  微信公众号  Tags:Kubernetes   点击:(6)  评论:(0)  加入收藏
站内最新
站内热门
站内头条