二、Jdk1.7和1.8中HashMap数据结构及源码分析

17 篇文章 2 订阅
9 篇文章 2 订阅

导读

上篇文章(一、深入理解-Java集合初篇 )中我们对Java的集合体系进行一个简单的分析,本篇我们我们将针对Java集合体系中的HashMap做详细介绍。
简单介绍

JDK1.7—》哈希表,链表

JDK1.8—》哈希表,链表,红黑树— JDK1.8之后,当链表长度超过8使用红黑树。

非线程安全

0.75的负载因子,扩容必须为原来的两倍。

默认大小为16,传入的初始大小必须为2的幂次方的值,如果不为也会变为2的幂次方的值。

根据HashCode存储数据。

JDK1.7-HashMap数据结构—》数组,链表

在这里插入图片描述

JDK1.7-HashMap源码分析:

源码版本:

C:\Users\bruce_y>java -version
java version "1.7.0_80"
Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)

JDK1.7-HashMap主要变量声明:

/**
 * The default initial capacity - MUST be a power of two.
HashMap的初始化容量大小。2的4次幂。
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
HashMap的最大容量大小,为2的30次幂。
为什么为2的30次幂。?后续又解读。
 */
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
 * The load factor used when none specified in constructor.
默认的负载因子,也就是扩容因子。
当HashMap的内数组容量达到 (设置容量  *  负载因子)时候,
HashMap进行扩容。扩容为原来的两倍。还是2的n次幂。
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
 * An empty table instance to share when the table is not inflated.
HashMap构造的时候的默认值。
由于HashMap中的数组是在第一次put的时候才会进行初始化,
因此在未put操作之前HashMap内数组结果为空。
 */
static final Entry<?,?>[] EMPTY_TABLE = {};
/**
 * The table, resized as necessary. Length MUST Always be a power of two.
HashMap的真实数组存放的数组,该值会动态变更,他的长度必须为2的n次幂。
他的默认值是一个空的值。
 */
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
/**
 * The number of key-value mappings contained in this map.
HashMap中存储数据的多少。
 */
transient int size;
/**
 * The next size value at which to resize (capacity * load factor).
 * @serial
HashMap 中数组的扩容门槛。当数组中数据量超过该值时候,
对HashMap的数组容量进行扩容。该值的大小为(容量  *  负载因子)
当 HashMap 为空的时候,也就是刚构造完的时候,该值为构造HashMap时候的初始容量。
在进行第一次put操作的时候会进行变动该值。
 */
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
int threshold;
/**
 * The load factor for the hash table.
 * HashMap 的负载因子。
当HashMap的内数组容量达到 (设置容量  *  负载因子)时候,
HashMap进行扩容。扩容为原来的两倍。还是2的n次幂。
 * @serial
 */
final float loadFactor;
/**
HashMap中结构修改的次数。
 * The number of times this HashMap has been structurally modified
 * Structural modifications are those that change the number of mappings in
 * the HashMap or otherwise modify its internal structure (e.g.,
 * rehash).  This field is used to make iterators on Collection-views of
 * the HashMap fail-fast.  (See ConcurrentModificationException).
 */
transient int modCount;
/**
决定是否使用干扰因子(hashSeed)对HashMap中出现的哈希冲突进行干扰的默认值。
后续会根据JDK的配置“jdk.map.althashing.threshold”
与该值进行比较后决定是否使用干扰因子(hashSeed)进行动态干扰。
文章后续内容对该值进行详细的介绍。
 * The default threshold of map capacity above which alternative hashing is
 * used for String keys. Alternative hashing reduces the incidence of
 * collisions due to weak hash code calculation for String keys.
 * <p/>
 * This value may be overridden by defining the system property
 * {@code jdk.map.althashing.threshold}. A property value of {@code 1}
 * forces alternative hashing to be used at all times whereas
 * {@code -1} value ensures that alternative hashing is never used.
 */
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
 
/**
HashMap中降低哈希冲突的干扰因子。如果使用该干扰因子,则该干扰因子是一个随机值。
文章后续内容对该值进行详细的介绍。
 * A randomizing value associated with this instance that is applied to
 * hash code of keys to make hash collisions harder to find. If 0 then
 * alternative hashing is disabled.
 */
transient int hashSeed = 0;
 

JDK1.7-创建HashMap:

创建时候,如果没有传入参数,则把默认的数组长度置为16,默认的负载因子置为0.75.

但是在创建的时候并没有进行数组的初始化,是在put操作的时候,进行的初始化。

initialCapacity=16//默认的初始容量为16---为2的4次方
loadFactor=0.75//默认的负载因素为不太精确的 0.75f构造
/**
/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
构造一个空的HashMap  默认初始化容量为16,默认的加载因子为0.75f.
 */
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
} /**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and load factor.
 *构造一个空的HashMap,并且传入指定的初始化容量和负载因子。
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)//如果传入的初始化容量小于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;//加载因子设置为传入的加载因子。默认0.75f.
 
//设置HashMap的初始化扩容门槛为初始化数组大小,该值默认为16.
//后续该值会变动,会变为容量*负载因子
    threshold = initialCapacity; 
    init();
}
 
/**
//可以看到 init()方法是空的,那么HashMap是在什么时候进行初始化的?
HashMap的初始化是在第一次进行put操作时候进行的。详细信息请看下一步。
 * Initialization hook for subclasses. This method is called
 * in all constructors and pseudo-constructors (clone, readObject)
 * after HashMap has been initialized but before any entries have
 * been inserted.  (In the absence of this method, readObject would
 * require explicit knowledge of subclasses.)
 */
void init() {
}
 

JDK1.7-HashMap的put操作

(一).在每次put操作时候,会判断当前的数组是否已经初始化,如果没有初始化则进行初始化

/**
 * Associates the specified value with the specified key in this map.
 * If the map previously contained a mapping for the key, the old
 * value is replaced.
 *
 * @param key key with which the specified value is to be associated
 * @param value value to be associated with the specified key
 * @return the previous value associated with <tt>key</tt>, or
 *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
 *         (A <tt>null</tt> return can also indicate that the map
 *         previously associated <tt>null</tt> with <tt>key</tt>.)
 */
public V put(K key, V value) {
//在每次put操作时候会判断当前 数组是否已经初始化,如果未初始化则进行初始化。
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);//真正进行数组初始化
    }
    if (key == null)//如果key为空,则把把它放入数组中第0号位置
        return putForNullKey(value);
    int hash = hash(key);//获取指定key的hash值
    int i = indexFor(hash, table.length);//根据哈希值和数组长度判断该插入的数组下标
//循环Entry链表,判断要插入的值是否已经存在于HashMap中,如果存在,则对该值进行替换。    
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
         //变更后返回旧值
            return oldValue;
        }
    }
//如果新添加的key,value不存在与HashMap中,则进行以下操作。
//增加hashMap中修改的次数
    modCount++;
//如果把新的值添加到HashMap中的指定位置中
    addEntry(hash, key, value, i);
    return null;
}

(二).初始化HashMap。

/**
 * Inflates the table.
 */
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize  强制变更为2的n次幂的值
    int capacity = roundUpToPowerOf2(toSize);
     //HashMap的容量门槛,也就是HashMap容量达到多少时候进行扩容。
//容量大小*负载因子的值  与  最大允许容量进行比较,哪个小使用哪个。
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化初始实体大小为容量大小    
table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}

1.变更传入值的大小,如果不为2的n次幂,则变更为2的n次幂。

如果传入的容量不为2的n次幂,则变更为大于传入数值且为2的n次幂的最小值。

例如:传入11,则大于11且大于2的n次幂的最小值为16.则把初始容量变更为16.

2的3次幂为8,2的4次幂为16. 2的5次幂为32.

private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

2.根据初始化的HashMap容量大小,决定干扰因子的值。

/**
初始化哈希干扰的掩码值,我们把它的设置延迟到了真正使用它的时候。 * Initialize the hashing mask value. We defer initialization until we
 * really need it. */
final boolean initHashSeedAsNeeded(int capacity) {
 
 
 
/**
判断是否开启了hash干扰。如果hashseed 等于 0,则currentAltHashing = false;
如果hashseed 不等于 0 ,则currentAltHashing = true;
**/ 
    boolean currentAltHashing = hashSeed != 0;
/** 
判断是否使用为干扰1.如果当前HashMap数组容量的大小等于jdk配置中“jdk.map.althashing.threshold”的值时候,
			且VM .isbooted 为true 时候,userAltHashing 为true;
2.如果当前HashMap数组容量的小于jdk配置中“jdk.map.althashing.threshold”的值时候,
			且VM .isbooted 为true 时候,userAltHashing 为false;
3. 如果当前HashMap数组容量的大小等于jdk配置中“jdk.map.althashing.threshold”的值时候,
			且VM .isbooted 为false 时候,userAltHashing 为false;
4. 如果当前HashMap数组容量的小于jdk配置中“jdk.map.althashing.threshold”的值时候,
			且VM .isbooted 为false 时候,userAltHashing 为false;
 **/
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
/** 
对以上的两个值进行“异或”处理异或的结果为 两个值相同为0,两个值不同为1.
也就是说 currentAltHashing 为true时且useAltHashing也为true时,
或者 currentAltHashing 为false时且useAltHashing也为false时
switching的结果为 true.其他情况时候,switching为false.
**/
    boolean switching = currentAltHashing ^ useAltHashing;
    if (switching) {
/**
如果switching为true ,设置hashSeed(干扰因子)的值。
如果useAltHashing的值为true,则随机一个干扰值给HashSeed。否则赋值为0.
**/ 
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
//返回是否启用干扰因子
    return switching;
}

当hashMap扩大容量时,都是调用该方法。从代码可以看出,当数组容量超过,我们设定的值ALTERNATIVE_HASHING_THRESHOLD且是vm booted,同时 hashSeed==0的时候,hashSeed的值就是用随机量,而不是固定的等于0。这样就能降低碰撞,就能降低演化成链表概率。

代码具体过程:

当 hashSeed==0 则 currentAltHashing=false

当 capacity < Holder.ALTERNATIVE_HASHING_THRESHOLD 则currentAltHashing =false

结果:

switching=false

当 hashSeed==0 则 currentAltHashing=false

当 capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD 则 currentAltHashing =true

结果:

switching=true

当 hashSeed !=0 则 currentAltHashing=true

当 capacity < Holder.ALTERNATIVE_HASHING_THRESHOLD 则 currentAltHashing =false

结果:

当 switching=true

当 hashSeed !=0 则 currentAltHashing=true

当 capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD 则 currentAltHashing =true

结果:

switching=false

回头再看代码,发现很巧妙

结论:

①.如果不配置“jdk.map.althashing.threshold”,则HashMap中的私有静态内部类中的’ALTERNATIVE_HASHING_THRESHOLD‘的值为Integer.MAX_VALUE,且HashSeed默认值为0,则switching的值永远为false,也就永远不会改变干扰因子(HashSeed)的值。
②.如果配置了“jdk.map.althashing.threshold”,则会根据当前HashMap中的数组容量动态的变更HashSeed的值,以便于引入HashSeed降低哈希冲突。
-Djdk.map.althashing.threshold=-1:表示不做优化(不配置这个值作用一样)
-Djdk.map.althashing.threshold<0:报错
-Djdk.map.althashing.threshold=1:表示总是启用随机HashSeed
-Djdk.map.althashing.threshold>=0:便是hashMap内部的数组长度超过该值了就使用随机HashSeed,降低碰撞

(三).获取key的Hash值,位干扰。

/**
 * Retrieve object hash code and applies a supplemental hash function to the
 * result hash, which defends against poor quality hash functions.  This is
 * critical because HashMap uses power-of-two length hash tables, that
 * otherwise encounter collisions for hashCodes that do not differ
 * in lower bits. Note: Null keys always map to hash 0, thus index 0.
 */final int hash(Object k) {
/** 获取哈希干扰因子,该因子会跟根据HashMap的容量进行变更变更情况根据上一步的“final boolean initHashSeedAsNeeded(int capacity)”方法动态变更**/     int h = hashSeed;//如果为干扰因子不为0,且传入的key类型为String,则使用特定的算法(sun.misc.Hashing.stringHash32((String) k))对该key进行hash计算。并返回
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }//如果哈希干扰因子为0 或者 k的类型不为String则使用异或操作变更key的hashcode    h ^= k.hashCode();//为了减少Hash冲突出现次数进行必要的位干扰,默认负载因子是8.    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded    // number of collisions (approximately 8 at default load factor).    h ^= (h >>> 20) ^ (h >>> 12);    return h ^ (h >>> 7) ^ (h >>> 4);
}

在hash(Object k)中有这么一段位运算的代码:

h ^= k.hashCode();

h ^= (h >>> 20) ^ (h >>> 12);

return h ^ (h >>> 7) ^ (h >>> 4);

看起来既简单又深奥的样子,让我们来看看这段代码隐藏的东西吧。

k.hashCode()函数调用的是key键值类型自带的哈希函数,由于不同的对象其hashCode()有可能相同,所以需对hashCode()再次哈希,以降低相同率。

接下来的一串与运算和异或运算,称之为“扰动函数”,扰动的核心思想在于使计算出来的值在保留原有相关特性的基础上,增加其值的不确定性,从而降低冲突的概率。不同的版本实现的方式不一样,但其根本思想是一致的。
这里的具体实现方式是如何保证的呢?笔者功力浅薄,暂时还没有答案,如果有朋友知道的话可以交流。但是,“扰动函数”的核心思想一定要明白。

(四).根据上一步获得的key的Hash值得到新插入的操作应该在的下标位置

int i = indexFor(hash, table.length);
/**
 * Returns index for hash code h.
 */
static int indexFor(int h, int length) {
//length长度必须为2的n次幂,且非0
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

下标值的的大小根据哈希值与(当前HashMap的数组长度-1)进行与操作后得到。

计算hash的时候不是用取模,而是用二进制按位与,然后计算hash值进入对应的值。

取模运算很消耗时间。 位运算效率更高。

位运算的前提 length的值必须是2的指数次幂。

0 <= hash&(lehth-1) < 16

让hash与长度按位与的时候都在0-16之间。

二进制位运算展示(64为,高位0,低位是值)

Hash 101010100 0111 1100 -hash值

length 000000000 0001 0000 -height16 -初始化长度

length-1 000000000 0000 1111 -height-1 =15 -数组下标

hashkey & (length -1)

不管hash值如何变,那么只需要看低位即可进入对应的hash数组。

最大值为15,最小值为0.即当前数组中的下标位置。

(五). 循环Entry链表,判断要插入的值是否已经存在于HashMap中,如果存在,则对该Entry的value进行替换。

for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    Object k;    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
        V oldValue = e.value;
        e.value = value;
        e.recordAccess(this);
        return oldValue;
    }}

(六). 如果新添加的key,value不存在于HashMap中,则进行以下操作。

//如果新添加的key,value不存与HashMap中,则进行以下操作。
//增加hashMap中修改的次数    modCount++;//如果把新的值添加到HashMap中的指定位置中    addEntry(hash, key, value, i);
    return null;

(七).把新添加的值添加到HashMap中。

//如果把新的值添加到HashMap中的指定位置中
addEntry(hash, key, value, i);
/**
添加一个新的entry到指定的哈希桶中。Entry的值已经做了明确的规定:hashcode,key,value。
添加新entry到哈希桶的过程中可能会对哈希表的大小进行扩容。
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket.  It is the responsibility of this
 * method to resize the table if appropriate.
 *
 * Subclass overrides this to alter the behavior of put method.
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
//如果当前哈希表大小超过了阀值,并且新entry要插入的哈希桶的位置不为空
//则把哈希表大小扩展为原来的两倍。
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //重新设置哈希表的大小
         resize(2 * table.length);
         //重新计算key的哈希值
        hash = (null != key) ? hash(key) : 0;
         //根据新的hash值和新的哈希表大小计算要放入的哈希桶的位置。
        bucketIndex = indexFor(hash, table.length);
    }
//创建一个新的entry,并把对应的值放到哈希桶的指定位置
    createEntry(hash, key, value, bucketIndex);
}

(八).根据条件判断是否对哈希表进行扩容

如果当前哈希表大小超过了阀值,并且新entry要插入的哈希桶的位置不为空

则把哈希表大小扩展为原来的两倍。

当哈希表中数据容量达到阀值,则使用一个新数组来获得一个更大的容量。

如果当前容量是允许的最大容量(2的30次幂),则该方法不会再继续扩大容量,

但是他会把负载门槛设置为Integer.MAX_VALUE.这只为了防止后续再进行扩容操作。

新传入的容量的值必须为2的n次幂,并且必须大于当前数组的容量。

如果当前数组容量已经允许的最大容量(2的30次幂),则新传入的值被忽略。

/**
当哈希表中数据容量达到阀值,则使用一个新数组来获得一个更大的容量。如果当前容量是允许的最大容量(2的30次幂),则该方法不会再继续扩大容量,
但是他会把负载门槛设置为Integer.MAX_VALUE.这只为了防止后续再进行扩容操作。 * Rehashes the contents of this map into a new array with a * larger capacity.  This method is called automatically when the
 * number of keys in this map reaches its threshold.
 * * If current capacity is MAXIMUM_CAPACITY, this method does not
 * resize the map, but sets threshold to Integer.MAX_VALUE. * This has the effect of preventing future calls. *新传入的容量的值必须为2的n次幂,并且必须大于当前数组的容量。
如果当前数组容量已经允许的最大容量(2的30次幂),则新传入的值被忽略。
 * @param newCapacity the new capacity, MUST be a power of two; *        must be greater than current capacity unless current
 *        capacity is MAXIMUM_CAPACITY (in which case value
 *        is irrelevant). */
void resize(int newCapacity) {
//把原哈希表数组赋值给oldTable
    Entry[] oldTable = table;
//把原哈希表容量赋值给oldCapacity
    int oldCapacity = oldTable.length;
//如果当前的哈希表容量已经达到允许的容量最大值(2的30次幂),则不再进行扩容
//且把当前哈希表的负载门槛设置为Integer的最大值。返回,跳过。
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
//创建一个新的哈希数组,容量为新传入的容量值
//该容量值必须是2的n次幂,且大于原数组容量大小
    Entry[] newTable = new Entry[newCapacity];
//开始把原哈希表数组数据转入新创建的哈希表数组中
//在开始转存前要先根据新的数组容量及相应的算法得出是否使用哈希干扰掩码
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
//转存完成后把新表内容放到HashMap的哈希表值中
    table = newTable;
//设置当前容量下的负载门槛
//(新容量 * 负载因子)的值与(HashMap允许的最大容量(2的30次幂)+1) 进行比较,
//取值小的那一个
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

哈希表数据转存:

把哈希表中的所有entry从当前的哈希表转入到新的哈希表中

**
 * Transfers all entries from current table to newTable. */
void transfer(Entry[] newTable, boolean rehash) {
     //获取新哈希表的容量
    int newCapacity = newTable.length;
//循环原哈希表
    for (Entry<K,V> e : table) {
//循环原Entry线性链表
        while(null != e) {
            Entry<K,V> next = e.next;
//根据是否启用rehash判断是否为每一个key生成新的哈希值
//如果当前entry的key等于null,则重新设置当前entry的哈希值为0
//如果不为null,则对当前entyr的哈希值根据哈希干扰因子(HashSeed)进行重
//新计算赋值
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
//根据新的哈希值和新的容量计算该entry应该存放的数组下标位置
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

HashMap的转存使用头部插入法。

分析图解:

1:为了方便计算,假设hash算法为key mod链表长度;

2:初始时数组长度2,key = 3, 7, 5 初始在表table[1]节点;

3:然后resize后,hash数组长度为4
    在这里插入图片描述

如果不发生异常,正常结果为:
在这里插入图片描述(九). 创建一个新的entry,并把对应的值放到哈希桶的指定位置

创建一个新的Entry,并使用链表的头部插入法,把新Entry插入链表的指定位置。

/**
 * Like addEntry except that this version is used when creating entries
 * as part of Map construction or "pseudo-construction" (cloning,
 * deserialization).  This version needn't worry about resizing the table.
 *
 * Subclass overrides this to alter the behavior of HashMap(Map),
 * clone, and readObject.
 */
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++;}

至此Put操作结束。

下一篇文章我们将要讲解JDK1.8中的构造方法和put方法。

往期文章链接

Java集合

一、深入理解-Java集合初篇

Java-IO体系

一、C10K问题经典问答
二、java.nio.ByteBuffer用法小结
三、Channel 通道
四、Selector选择器
五、Centos-Linux安装nc
六、windows环境下netcat的安装及使用
七、IDEA的maven项目的netty包的导入(其他jar同)
八、JAVA IO/NIO
九、网络IO原理-创建ServerSocket的过程
十、网络IO原理-彻底弄懂IO
十一、JAVA中ServerSocket调用Linux系统内核
十二、IO进化过程之BIO
十三、Java-IO进化过程之NIO
十四、使用Selector(多路复用器)实现Netty中Reactor单线程模型
十五、使用Selector(多路复用器)实现Netty中Reactor主从模型
十六、Netty入门服务端代码

如需了解更多更详细内容也可关注本人CSDN博客:不吃_花椒

Java集合还需要学习的内容

在这里插入图片描述
在这里插入图片描述在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值