https://www.cnblogs.com/vitasyuan/p/9220773.html

1.HashMap-1.8介绍

HashMap为Map接口的一个实现类,实现了所有Map的操作。HashMap除了允许key和value保存null值和非线程安全外,其他实现几乎和HashTable一致。

HashMap使用散列存储的方式保存kay-value键值对,因此其不支持数据保存的顺序。如果想要使用有序容器可以使用LinkedHashMap。

在性能上当HashMap中保存的key的哈希算法能够均匀的分布在每个bucket中的是时候,HashMap在基本的get和set操作的的时间复杂度都是O(n)。

在遍历HashMap的时候,其遍历节点的个数为bucket的个数+HashMap中保存的节点个数。因此当遍历操作比较频繁的时候需要注意HashMap的初始化容量不应该太大。

这一点其实比较好理解:当保存的节点个数一致的时候,bucket越少,遍历次数越少。

另外HashMap在resize的时候会有很大的性能消耗,因此当需要在保存HashMap中保存大量数据的时候,传入适当的默认容量以避免resize可以很大的提高性能。

具体的resize操作请参考下面对此方法的分析

HashMap是非线程安全的类,当作为共享可变资源使用的时候会出现线程安全问题。需要使用线程安全容器:

Map m = new ConcurrentHashMap();或者Map m = Collections.synchronizedMap(new HashMap());

具体的HashMap会出现的线程安全问题分析请参考9中的分析。

2.数据结构介绍

HashMap使用数组+链表+树形结构的数据结构。其结构图如下所示。

3.HashMap源码分析(基于JDK1.8)

3.1关键属性分析

  transient Node<K,V>[] table;

    Node类型的数组,记我们常说的bucket数组,其中每个元素为链表或者树形结构

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

    保存缓存的entrySet()

  transient int size;

    HashMap中保存的数据个数

  transient int modCount;

    此哈希映射在结构上被修改的次数

  int threshold;

    HashMap需要resize操作的阈值

  final float loadFactor;

    负载因子,用于计算threshold。计算公式为:threshold = loadFactor * capacity

  备注:有默认容量capacity  2^4 = 16,默认扩容负载因子loadFactor=0.75等.用于构造函数没有指定数值情况下的默认值。

3.2Node分析

他是Hashmap的内部类,存储key,value键值对

1.4个参数

  Hash - final常量

  Key  - final常量

  value  -  值

  Node<K,V> next;

2.只有有参构造

3.key被final修饰-不能修改-只能创建的时候赋值

4.重写equals方法:key和value都相等(地址值相等)返回true

 static class Node<K,V> implements Map.Entry<K,V> {

        final int hash; //final修饰-常量-不可变

        final K key; //final修饰-常量-不可变

        V value;

        Node<K,V> next;  //下一个元素,形成链表

        Node(int hash, K key, V value, Node<K,V> next) {//只有有参构造没有无参构造

            this.hash = hash;

            this.key = key;

            this.value = value;

            this.next = next;

        }

        public final K getKey()        { return key; }

        public final V getValue()      { return value; }

        public final String toString() { return key + "=" + value; }

        public final int hashCode() {

            return Objects.hashCode(key) ^ Objects.hashCode(value);

        }

        public final V setValue(V newValue) { //value设值:旧值新值被覆盖且返回旧值

            V oldValue = value;

            value = newValue;

            return oldValue;

        }

        public final boolean equals(Object o) {//key和value相等(地址值)

            if (o == this)

                return true;

            if (o instanceof Map.Entry) {

                Map.Entry<?,?> e = (Map.Entry<?,?>)o;

                if (Objects.equals(key, e.getKey()) &&

                    Objects.equals(value, e.getValue()))

                    return true;

            }

            return false;

        }

    }

3.3关键函数源码分析

3.3.1构造函数

无参构造:默认扩容负载因子0.75

有参构造:最多两个参数 1.cap初始容量  2.loadFactor 扩容因子

有参构造虽然传入cap的值,但是没有创建tab,只是对阀值threshold 进行了赋值,赋值为传入cap最近的大于等于cap的2的整数次幂(这个值resize的时候会赋值给cap,保证cap是2的整数次幂),这个时候不创建tab(cap=tab.length就还没有确定)是为了节省空间

HashMap提供了三个不同的构造函数

static final int MAXIMUM_CAPACITY = 1 << 30;  最大容量

public HashMap() {

         this.loadFactor = DEFAULT_LOAD_FACTOR;

    //无参构造-默认扩容负载因子0.75
//static final float DEFAULT_LOAD_FACTOR = 0.75f; } public HashMap(int initialCapacity) { //有参-设定初始容量大小,默认扩容负载因子0.75
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;
this.threshold = tableSizeFor(initialCapacity);
//tableSizeFor:返回大于输入参数且最近的2的整数次幂的数(当这个值大于最大容量MAXIMUM_CAPACITY时,返回MAXIMUM_CAPACITY)。
//注意:此处的initialCapacity为数组table的大小,即bucket的个数。另外此处赋值为this.threshold,是因为构造函数的时候并不会创建table,
//只有实际插入数据的时候才会创建。目的应该是为了节省内存空间。
//在第一次插入数据的时候,会将table的capacity设置为threshold,同时将threshold更新为loadFactor * capacity
}

tableSIzeFor()方法

tableSizeFor的功能:返回大于输入参数且最近的2的整数次幂的数(当这个值大于最大容量MAXIMUM_CAPACITY时,返回MAXIMUM_CAPACITY)。比如10,则返回16。该算法源码如下

1 static final int tableSizeFor(int cap) {
2 int n = cap - 1;
3 n |= n >>> 1;
4 n |= n >>> 2;
5 n |= n >>> 4;
6 n |= n >>> 8;
7 n |= n >>> 16;
8 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
9 }

先来分析有关n位操作部分:先来假设n的二进制为01xxx...xxx。接着

对n右移1位:001xx...xxx,再位或:011xx...xxx

对n右移2为:00011...xxx,再位或:01111...xxx

此时前面已经有四个1了,再右移4位且位或可得8个1

同理,有8个1,右移8位肯定会让后八位也为1。

综上可得,该算法让最高位的1后面的位全变为1。

最后再让结果n+1,即得到了2的整数次幂的值了。

现在回来看看第一条语句:

int n = cap - 1;

  让cap-1再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。

hash(Object key)方法

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

看一个方法indexFor,在jdk1.7中有indexFor(int h, int length)方法。jdk1.8里没有,但原理没变,1.8中用tab[(n - 1) & hash]代替但原理一样。下面看下1.7源码

static int indexFor(int h, int length) {
return h & (length-1);
}

h:key 调用hash()获取的值,length - node数组的长度,即hashmap的capacity容量大小

indexFor这个方法返回的是这个键值对在数组中存储的位置的下标,也就是说下标的结果与hash值有关

而h & (length-1)这个计算有个规律:

当length=8时    下标运算结果取决于哈希值的低三位

当length=16时  下标运算结果取决于哈希值的低四位

当length=32时  下标运算结果取决于哈希值的低五位

当length=2的N次方, 下标运算结果取决于哈希值的低N位

如果hash()方法中不进行>>> 和 ^运算,在大多数情况下,length的值(hashma的容量)小于2^16次方,根据上面的规律,hash值的高16位是没有参与下标的结果的。那么这样子会导致获取的下标不够分散均匀。所以对key.hashCode()进行>>>和^运算后,再去进行h & (length-1)运算,那么高16位就参与了下标的结果

例如1:为了方便验证,假设length为8。HashMap的默认初始容量为16

length = 8;  (length-1) = 7;转换二进制为111;

假设一个key的 hashcode = 78897121 转换二进制:100101100111101111111100001,与(length-1)& 运算如下

0000 0100 1011 0011 1101 1111 1110 0001

&运算

0000 0000 0000 0000 0000 0000 0000 0111

=   0000 0000 0000 0000 0000 0000 0000 0001 (就是十进制1,所以下标为1)

上述运算实质是:001 与 111 & 运算。也就是哈希值的低三位与length与运算。如果让哈希值的低三位更加随机,那么&结果就更加随机,如何让哈希值的低三位更加随机,那么就是让其与高位异或。

3. 原因总结

由于和(length-1)运算,length 绝大多数情况小于2的16次方。所以始终是hashcode 的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列。

所以这样高16位是用不到的,如何让高16也参与运算呢。所以才有hash(Object key)方法。让他的hashCode()和自己的高16位^运算。所以(h >>> 16)得到他的高16位与hashCode()进行^运算。

4. 为什么用^而不用&和|

因为&和|都会使得结果偏向0或者1 ,并不是均匀的概念,所以用^。

这就是为什么有hash(Object key)的原因。

3.3.2put方法

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
} 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)
      //1.首次添加,直接扩容resize()
n = (tab = resize()).length;
      //table 是参数 - transient Node<K,V>[] table;
if ((p = tab[i = (n - 1) & hash]) == null)
      //2.数组该下表没有元素,直接添加
tab[i] = newNode(hash, key, value, null);
else {
      //3.数组该下标有元素
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
        //3.1数组该下标元素key与put的key一致(单个元素或者链表的首个元素的key和put的key一致),e取旧的元素,e的value下面回赋值
e = p;
else if (p instanceof TreeNode)
        //3.2数组该下标元素为红黑树-处理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
        //3.2数组该下标元素为链表且第一个元素与put的key不一致/数组该下标只有一个元素且key与put的key不一致
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //当链表下一个元素为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))))
         //链表的下一个元素的key和put的key一致,结束循环
break;
p = e;
}
}
if (e != null) { // e-用于put的key和数组原元素相同时,记录旧的元素,这里吧put的value更新到元素中
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

3.3.3resize方法

无参构造:默认扩容负载因子0.75

有参构造:最多两个参数 1.cap初始容量  2.loadFactor 扩容因子

虽然传入cap的值,但是没有创建tab,只是对阀值threshold 进行了赋值,赋值为传入cap最近的大于等于cap的2的整数次幂

(这个值会赋值给cap,保证cap是2的整数次幂),这个时候不创建tab(cap=tab.length就还没有确定)是为了节省空间

resize:

第一次扩容:

无参构造创建: cap:2^4  Thr:cap * 0.75

有参构造创建: 初始容量设为阀值阀值设置为threshold = cap * loadFactor

非第一次扩容:2倍扩容(最大值为MAXIMUM_CAPACITY = 1 << 30)

阀值等于threshold = cap * loadFactor

  1 final Node<K,V>[] resize() {
2
3 Node<K,V>[] oldTab = table; //旧的node数组
4
5 int oldCap = (oldTab == null) ? 0 : oldTab.length;//旧的数组容量
6
7 int oldThr = threshold;//旧的扩容阀值
8
9 int newCap, //新的容量
10
11      newThr = 0; //新的扩容阀值
12
13 if (oldCap > 0) { //1.原来容量不为0(不是第一次添加,已经扩过融了)
14
15 if (oldCap >= MAXIMUM_CAPACITY) {
16
17         // static final int MAXIMUM_CAPACITY = 1 << 30;
18
19         //如果旧容量大于等于1^30,扩容机制直接取int最大值2^32
20
21 threshold = Integer.MAX_VALUE;
22
23 return oldTab; //不扩容,直接返回旧的数组
24
25 }
26
27 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
28
29 oldCap >= DEFAULT_INITIAL_CAPACITY)
30
31 newThr = oldThr << 1; // double threshold
32
33           //static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
34
35           //如果旧的数组容量2倍小于最大容量并且旧的容量大于默认容量2^4,新的扩容阀值等于旧的2倍
36 }
37
38 else if (oldThr > 0)
39     //2.有参构造常见的第一次扩容 - 初始容量设置为阀值,初始化的时候有参构造传入了容量大小,但是初始化的时候只设置了threshold
40     //的值而没有设置capacity的值,而threshold 取得是大于等于传入容量大小的离他最近的一个2的整次幂的值,
41     //保证threshold 是2的整次幂,此时将threshold 赋值给capacity,保证capacity是2的整次幂
42 newCap = oldThr;
43 else {
44     // 3.无参构造创建的,第一次添加
45 newCap = DEFAULT_INITIAL_CAPACITY; //默认值 2^4
46 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//默认值 0.75 * 2 ^ 4 = 12 默认扩容阀值
47 }
48
49 if (newThr == 0) {
50     //扩容阀值等于容量乘以扩容因子最大值为最大int
51 float ft = (float)newCap * loadFactor;
52 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
53 (int)ft : Integer.MAX_VALUE);
54 }
55
56 threshold = newThr;
57
58 @SuppressWarnings({"rawtypes","unchecked"})
59
60 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
61
62 table = newTab;
63
64 if (oldTab != null) { //把旧的数组的Node放到新的数组里面
65
66 for (int j = 0; j < oldCap; ++j) {
67
68 Node<K,V> e;
69
70 if ((e = oldTab[j]) != null) { //旧的Node元素不为null时,赋值给e
71
72 oldTab[j] = null;
73
74 if (e.next == null) //Node对象中的参数 Node<K,V> next为null,说明这里不是链表结构,原数组这里只有一个元素
75
76 newTab[e.hash & (newCap - 1)] = e;//e放入新数组中
77
78 else if (e instanceof TreeNode)//如果是红黑树树形结构,红黑树的重定位;
79
80
81
82 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
83
84 else { //当前是链表
85
86 Node<K,V> loHead = null, //用于接收新数组中下标为j这里的元素
87
88               loTail = null;
89
90 Node<K,V> hiHead = null, //用于结束新数组中下标为j+oldcap的元素
91
92               hiTail = null;
93
94 Node<K,V> next;
95
96 do {
97
98 next = e.next;
99
100 if ((e.hash & oldCap) == 0) { //处理新数组中下表为j的,这里结果是0的话,代表e.hash/oldcap的结果是2的倍数+余数,扩容后e.hash/2oldcap,余数不变,所以下标不变
101
102 if (loTail == null)
103
104 loHead = e;
105
106 else
107
108 loTail.next = e;
109
110 loTail = e;
111
112 }
113
114 else { //用于处理新数组中下表为j+capacity的
115
116 if (hiTail == null)
117
118 hiHead = e;
119
120 else
121
122 hiTail.next = e;
123
124 hiTail = e;
125
126 }
127
128 } while ((e = next) != null);
129
130 if (loTail != null) { //新数组线标j元素赋值
131
132 loTail.next = null;
133
134 newTab[j] = loHead;
135
136 }
137
138 if (hiTail != null) {//新数组线标j+oldcap元素赋值
139
140 hiTail.next = null;
141
142 newTab[j + oldCap] = hiHead;
143
144 }
145
146 }
147
148 }
149
150 }
151
152 }
153
154 return newTab;
155
156 }

resize链表处理关键代码

 1                  if ((e.hash & oldCap) == 0) {
2 if (loTail == null)
3 loHead = e;
4 else
5 loTail.next = e;
6 loTail = e;
7 }
8 else {
9 if (hiTail == null)
10 hiHead = e;
11 else
12 hiTail.next = e;
13 hiTail = e;
14 }
15 } while ((e = next) != null);

如上图:

  

  链表处理,把链表分为了两块,1.在型数组中与原数组下标相同的,2.在型数组中下标=原数组下标+原数组容量,我们只看第一种:新数组中下标和原数组中相同的(第二种是一样的):

  加入现在这个链表有4节   e1-e2-e3-e4,

  if ((e.hash & oldCap) == 0)这个判断分别是true true false true,

  那么参数e lohead lotail在上图中4次判断的内存情况分别为 黑色  绿色  黄色  红色

    第一次true, e指向 e1 lohead 指向e1 lotail指向e1

    第二次true, e = next指向e2 ,loTail.next = e---litail原指向e1,相当于e1.next=e=e2;loTail = e;lotail指向e2

    第三次false,e = next指向e3,其它两个不变

    第四次true:e = next指向e4,loTail.next = e --- lotail原指向e2,相当于e2.next = e = e4,loTail = e;lotail指向e4

    那么最终的结果就是红色的线条,我们看lohead的指向lohead = e1    e1.next = e2    e2.next = e4,所以它现在是e1-e2-e4,就是我们想要的结果。

3.4Cloneable和Serializable分析

在HashMap的定义中实现了Cloneable接口,Cloneable是一个标识接口,主要用来标识 Object.clone()的合法性,在没有实现此接口的实例中调用 Object.clone()方法会抛出CloneNotSupportedException异常。可以看到HashMap中重写了clone方法。

HashMap实现Serializable接口主要用于支持序列化。同样的Serializable也是一个标识接口,本身没有定义任何方法和属性。另外HashMap自定义了

private void writeObject(java.io.ObjectOutputStream s) throws IOException

private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException

两个方法实现了自定义序列化操作。

注意:支持序列化的类必须有无参构造函数。这点不难理解,反序列化的过程中需要通过反射创建对象。

4.HashMap线程不安全问题

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) // 如果没有hash碰撞则直接插入元素
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;
}

其中第6行代码是判断是否出现hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

除此之前,还有就是代码的倒数第4行处有个++size,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到第38行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。

HashMap简要介绍的更多相关文章

  1. 简要介绍BASE64、MD5、SHA、HMAC几种方法。

    加密解密,曾经是我一个毕业设计的重要组件.在工作了多年以后回想当时那个加密.解密算法,实在是太单纯了.     言归正传,这里我们主要描述Java已经实现的一些加密解密算法,最后介绍数字证书.     ...

  2. Java 集合系列10之 HashMap详细介绍(源码解析)和使用示例

    概要 这一章,我们对HashMap进行学习.我们先对HashMap有个整体认识,然后再学习它的源码,最后再通过实例来学会使用HashMap.内容包括:第1部分 HashMap介绍第2部分 HashMa ...

  3. Java 集合系列 09 HashMap详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  4. [转]Android系统Surface机制的SurfaceFlinger服务简要介绍和学习计划

    转自:Android系统Surface机制的SurfaceFlinger服务简要介绍和学习计划 前面我们从Android应用程序与SurfaceFlinger服务的关系出发,从侧面简单学习了Surfa ...

  5. [转] Android资源管理框架(Asset Manager)简要介绍和学习计划

    转自:http://blog.csdn.net/luoshengyang/article/details/8738877 Android应用程序主要由两部分内容组成:代码和资源.资源主要就是指那些与U ...

  6. Activity启动过程简要介绍

    无论是通过点击应用程序图标来启动Activity,还是通过Activity内部调用startActivity接口来启动新的Activity,都要借助于应用程序框架层的ActivityManagerSe ...

  7. Android应用程序的Activity启动过程简要介绍和学习计划

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6685853 在Android系统中,Activ ...

  8. Dalvik虚拟机简要介绍和学习计划

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/8852432 我们知道,Android应用程序是 ...

  9. Android资源管理框架(Asset Manager)简要介绍和学习计划

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/8738877 Android应用程序主要由两部分 ...

  10. Android应用程序组件Content Provider简要介绍和学习计划

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6946067 在Android系统中,Conte ...

随机推荐

  1. 设计模式学习(二十四):Spring 中使用到的设计模式

    设计模式学习(二十四):Spring 中使用到的设计模式 作者:Grey 原文地址: 博客园:设计模式学习(二十四):Spring 中使用到的设计模式 CSDN:设计模式学习(二十四):Spring ...

  2. Installing harbor-2.6.2 on openEuler

    一.Installing harbor-2.6.2 on openEuler 1 地址 https://goharbor.io https://github.com/goharbor/harbor 2 ...

  3. Kubernetes—资源管理

    3. 资源管理 3.1 资源管理介绍 在kubernetes中,所有的内容都抽象为资源,用户需要通过操作资源来管理kubernetes. kubernetes的本质上就是一个集群系统,用户可以在集群中 ...

  4. 图文并茂解释开源许可证GPL、BSD、MIT、Mozilla、Apache和LGPL的区别

    世界上的开源许可证(Open Source License)大概有上百种,而我们常用的开源软件协议大致有GPL.BSD.MIT.Mozilla.Apache和LGPL. 从下图中可以看出几种开源软件协 ...

  5. 【Devexpress】Gridcontorl的列隐藏后再显示位置发生了变化

    首先在可视化界面中排序好每个列的显示位置索引 在窗口初始化时进行记录在字段中 /// <summary> /// 当前显示列的位置索引,用于隐藏后显示进行重新排序位置 /// </s ...

  6. layui文件上传+ThinkPHP

    1.前端html代码 <div class="layui-form-item"> <label class="layui-form-label" ...

  7. Linux下用rm误删除文件的三种恢复方法

    Linux下用rm误删除文件的三种恢复方法 对于rm,很多人都有惨痛的教训.我也遇到一次,一下午写的程序就被rm掉了,幸好只是一个文件,第二天很快又重新写了一遍.但是很多人可能就不像我这么幸运了.本文 ...

  8. 【面试题总结】JVM01-组成及垃圾回收

    一.概念 1.JVM组成及作用 (1)组成:类加载器.运行时数据区(Java内存模型).执行引擎.本地库接口 (2)作用: 类加载器(ClassLoader)把class文件转换成字节码: 运行时数据 ...

  9. elasticsearch global 、 filters 和 cardinality 聚合

    目录 1. 背景 2.解释 1.global 2.filters 3.cardinality 3.需求 4.前置条件 4.1 创建mapping 4.2 准备数据 5.实现3的需求 5.1 dsl 5 ...

  10. 【机器学习】李宏毅——Transformer

    Transformer具体就是属于Sequence-to-Sequence的模型,而且输出的向量的长度并不能够确定,应用场景如语音辨识.机器翻译,甚至是语音翻译等等,在文字上的话例如聊天机器人.文章摘 ...