线程

1. 线程休眠

需求:编写一个抽取学员回答问题的程序,要求倒数三秒后输出被抽中的学员姓名

Thread.sleep(1000);

此方法为静态方法,写在哪个线程中,哪个线程就休眠

package com.wz.thread06;

import java.util.Random;

public class test01 {
    /**
     * 知识点:线程的休眠
     * 需求:编写一个抽取学员回答问题的程序
     * 要求倒数三秒后输出被抽中的学员姓名
     */
    public static void main(String[] args) throws InterruptedException {
        String[] name = {"张三","李四","王五","赵六","AAA"};
        Random ran = new Random();
        int index=ran.nextInt(name.length);

        for (int i = 3; i >= 1; i--) {
            System.out.println(i);
            Thread.sleep(100);
        }
        System.out.println("被抽中的学生为:"+ name[index]);
        
    }
}

2023年6月30日,线程,Vector底层,HashMap底层_线程守护

2. 线程的礼让

需求:创建两个线程A,B,分别各打印1-100的数字,其中B一个线程,每打印一次,就礼让一次,观察实验结果

Thread.yield();

此方法为静态方法,此方法写在哪个线程中,哪个线程就礼让

所谓的礼让是指当前线程退出CPU资源,并转到就绪状态,接着再抢

package com.wz.thread07;

public class A extends Thread{
    @Override
    public void run() {
        for (int i=1;i<=100;i++){
            System.out.println("A"+i);
        }
    }
}
package com.wz.thread07;

public class B extends Thread{
    @Override
    public void run() {
        for (int i = 1; i <=100 ; i++) {
            System.out.println("B"+i);
            //礼让
            Thread.yield();
        }
    }
}
package com.wz.thread07;

public class test01 {
    /**
     * 知识点:线程的礼让
     * 需求:创建两个线程A,B,分别各打印1-100的数字,
     * 其中B一个线程,每打印一次,就礼让一次,观察实验结果
     */
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        a.start();
        b.start();
    }
}

3. 线程的合并

需求:主线程和子线程各打印200次,从1开始每次增加1,当主线程打印到10之后,让子线程先打印完再打印主线程

t.join(); 合并方法

package com.wz.thread08;

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 1; i <=200 ; i++) {
            System.out.println("子线程"+i);
        }
    }
}
package com.wz.thread08;

public class test01 {
    /**
     * 需求:主线程和子线程各打印200次,从1开始每次增加1,
     * 当主线程打印到10之后,让子线程先打印完再打印主线程
     */
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.start();
        for (int i = 1; i <=200 ; i++) {
            System.out.println("主线程"+i);
            if (i == 10){
                t.join();
            }
        }
    }
}

2023年6月30日,线程,Vector底层,HashMap底层_线程守护_02

4. 线程的中断

在Java中,线程的中断是通过调用线程的interrupt()方法来触发的。当一个线程被中断时,它的中断状态会被设置为"中断"。

可以通过Thread.currentThread().isInterrupted()方法来查询当前线程的中断状态,或者通过Thread.interrupted()方法来检查当前线程的中断状态,并且清除中断状态,以便下一次中断能够被正确地检测到。

以下是一些常见的线程中断的应用场景和用法:

  1. 终止循环:在任务的主循环中,可以通过检查中断状态来决定是否继续执行任务。例如:
while (!Thread.currentThread().isInterrupted()) {
    // 执行任务
    //Thread.currentThread().isInterrupted()判断当前线程是否销毁,销毁是true,未销毁是false,注意:置反
}
  1. 响应中断:在执行阻塞操作时,如果线程被中断,可以通过捕获InterruptedException异常来响应中断,并进行相应的处理。例如:
try {
    while (!Thread.currentThread().isInterrupted()) {
        // 执行可中断的阻塞操作,如Thread.sleep(), Object.wait(), Lock.lockInterruptibly()等
    }
} catch (InterruptedException e) {
    // 响应中断
}
  1. 中断其他线程:可以通过调用其他线程对象的interrupt()方法来中断该线程的执行。例如:x
Thread otherThread = new Thread(() -> {
    while (!Thread.currentThread().isInterrupted()) {
        // 执行任务
    }
});

otherThread.start();

// 中断其他线程
otherThread.interrupt();

需要注意的是,中断只是一种线程间的协作机制,它不能直接强制终止线程的执行。线程在被中断时,应该根据具体的应用场景和需求,选择合适的方式来停止任务的执行,例如通过设置一个标志位来终止循环、关闭资源等。

5. 线程的守护

守护线程 默默守护着前台线程,当所有的前台线程都消亡后,守护线程会自动消亡

注意:垃圾回收器就是守护线程

t.setDaemon(true);

package com.qf.thread13;

public class MyThread extends Thread{

	@Override
	public void run() {
		while(true){
			System.out.println("守护线程正在默默守护着前台线程...");
			
			//run方法是重写父类的,父类没有抛异常,子类就不能抛异常,只能try...catch
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}
package com.qf.thread13;

public class Test01 {
	/**
	 * 知识点:守护线程/后台线程
	 * 
	 * 理解:守护着前台线程,当程序中所有的前台线程消亡,守护线程也自动消亡
	 * 注意:Java的垃圾回收器就是个守护线程
	 */
	public static void main(String[] args) throws InterruptedException {
		
		MyThread t = new MyThread();
		t.setDaemon(true);//将线程设置为守护线程
		t.start();
		
		for (int i = 1; i <= 5; i++) {
			System.out.println("主线程:" + i);
			Thread.sleep(1000);
		}
	}
}

2023年6月30日,线程,Vector底层,HashMap底层_Vector底层_03

6. 线程局部变量(实现线程范围内的共享变量)

将要共享的变量存起来。在各个类中取出来。达到共享的目的

使用集合

key--线程对象

value--共享的值

使用ConcurrentHashMap(线程安全)

package com.qf.thread15;
public class A {
	public void print(){	
		//获取当前线程的对象
		Thread thread = Thread.currentThread();
		
		Data value = Test01.map.get(thread);
		System.out.println(thread.getName() + "中的A类对象获取到数据:" + value);
	}
}
package com.qf.thread15;
public class B {
	public void print(){
		//获取当前线程的对象
		Thread thread = Thread.currentThread();
		Data value = Test01.map.get(thread);
		System.out.println(thread.getName() + "中的B类对象获取到数据:" + value);
	}
}
package com.qf.thread15;

//数据包类
public class Data {

	private int i;
	private String str;
	
	public Data() {
	}
	
	public Data(int i, String str) {
		super();
		this.i = i;
		this.str = str;
	}

	public int getI() {
		return i;
	}

	public void setI(int i) {
		this.i = i;
	}

	public String getStr() {
		return str;
	}

	public void setStr(String str) {
		this.str = str;
	}

	@Override
	public String toString() {
		return "Data [i=" + i + ", str=" + str + "]";
	}
}
package com.qf.thread15;

import java.util.concurrent.ConcurrentHashMap;

public class Test01 {
	/**
	 * 知识点:线程局部变量共享 -- 共享多个数据
	 */
	
	public static final ConcurrentHashMap<Thread, Data> map = new ConcurrentHashMap<>();
	
	public static void main(String[] args) {
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				Data value = new Data(10, "初心至善");
				
				map.put(Thread.currentThread(), value);
				
				A a = new A();
				B b = new B();
				a.print();//10
				b.print();//10
			}
		}, "线程1").start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				Data value = new Data(20, "匠心育人");
				
				map.put(Thread.currentThread(), value);
				
				A a = new A();
				B b = new B();
				a.print();//20
				b.print();//20
			}
		}, "线程2").start();		
	}
}

使用ThreadLocal实现

package com.qf.thread16;

public class A {

	public void print(){
		
		//获取当前线程的对象
		Thread thread = Thread.currentThread();
		
		Data value = Test01.local.get();
		System.out.println(thread.getName() + "中的A类对象获取到数据:" + value);
	}
}
package com.qf.thread16;

public class B {

	public void print(){

		//获取当前线程的对象
		Thread thread = Thread.currentThread();

		Data value = Test01.local.get();
		System.out.println(thread.getName() + "中的B类对象获取到数据:" + value);

	}
}
package com.qf.thread16;

//数据包类
public class Data {

	private int i;
	private String str;
	
	private Data() {
	}
	
	private Data(int i, String str) {
		super();
		this.i = i;
		this.str = str;
	}
	
	//获取数据包对象的方法
	//目的:保证当前线程只有一个数据包对象
	public static Data getInstance(int i ,String str){
		//获取当前线程共享的数据包对象
		Data data = Test01.local.get();
		if(data == null){//说明当前线程没有共享的数据包对象
			//创建数据包对象,并存入local中
			data = new Data(i, str);
			Test01.local.set(data);
		}else{//说明当前线程有共享的数据包对象
			//更新数据
			data.setI(i);
			data.setStr(str);
		}
		return data;
	}

	public int getI() {
		return i;
	}

	public void setI(int i) {
		this.i = i;
	}

	public String getStr() {
		return str;
	}

	public void setStr(String str) {
		this.str = str;
	}

	@Override
	public String toString() {
		return "Data [i=" + i + ", str=" + str + "]";
	}
}
package com.qf.thread16;

public class Test01 {
	/**
	 * 知识点:线程局部变量共享 -- 共享多个数据
	 * 
	 * 注意:使用ThreadLocal去实现该需求
	 * 
	 * ThreadLocal是如何实现线程共享的?
	 * 	1.通过当前线程获取到ThreadLocal中的map
	 * 	2.ThreadLocal中的map的key为ThreadLocal对象,value存储的是要共享的值
	 */
	
	public static final ThreadLocal<Data> local = new ThreadLocal<>();
	
	public static void main(String[] args) {
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				Data value = Data.getInstance(10, "初心至善");
				value = Data.getInstance(100, "初心至善哈哈哈");
				
				local.set(value);
				
				A a = new A();
				B b = new B();
				a.print();//10
				b.print();//10
			}
		}, "线程1").start();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				Data value = Data.getInstance(20, "匠心育人");
				
				local.set(value);
				
				A a = new A();
				B b = new B();
				a.print();//20
				b.print();//20
			}
		}, "线程2").start();
	}
}

7. 线程的生命周期

1、新建状态

i. 在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时,它已经有了相应的内存空间和其它资源,但还处于不可运行状态。新建一个线程对象可采用线程构造方法来实现。

ii. 例如:Thread thread=new Thread();

2、 就绪状态

i. 新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU调用,这表明它已经具备了运行条件。

3、运行状态

i. 当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的run()方法。run()方法定义了该线程的操作和功能。

4、 阻塞状态

i. 一个正在执行的线程在某些特殊情况下,如被人为挂起,将让出CPU并暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep(2000)、wait()等方法,线程都将进入阻塞状态。阻塞时,线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

5、死亡状态

i. 线程调用stop()方法时或run()方法执行结束后,线程即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。

2023年6月30日,线程,Vector底层,HashMap底层_线程生命周期_04

Vector底层

public interface Enumeration<E> {
    //判断是否有下一个可迭代的元素
    boolean hasMoreElements();

    //获取下一个元素
    E nextElement();
}

Vector类的实现。

  1. AbstractList<E>:抽象类,实现了List接口,并继承了AbstractCollection抽象类。其中,modCount用于记录外部操作数。
  2. Vector<E>:实现了List接口,继承了AbstractList抽象类。实现了一个可变大小的数组。
  3. elementData:数据容器,用于存储元素的数组。
  4. elementCount:元素的个数。
  5. capacityIncrement:容量增量,用于确定在需要增加容量时,容量的增加量。
  6. Vector():无参构造方法,创建一个初始容量为10的Vector对象。
  7. Vector(int initialCapacity):带有初始容量参数的构造方法,创建一个指定初始容量的Vector对象。
  8. Vector(int initialCapacity, int capacityIncrement):带有初始容量和容量增量参数的构造方法,创建一个指定初始容量和容量增量的Vector对象。
  9. add(E e):向Vector末尾添加一个元素。首先增加外部操作数modCount,然后通过ensureCapacityHelper方法确保容量足够,最后将元素添加到elementData数组的末尾。
  10. ensureCapacityHelper(int minCapacity):检查是否需要扩容。如果需要扩容,则调用grow方法进行扩容。
  11. grow(int minCapacity):扩容数组。根据容量增量和当前容量计算新的容量,然后使用Arrays.copyOf方法将原数组的元素复制到新数组中。
  12. elementData(int index):根据索引获取元素。
  13. elements():返回一个Enumeration对象,用于遍历Vector中的元素。
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    //外部操作数
    protected transient int modCount = 0;//3
}

public class Vector<E> extends AbstractList<E> implements List<E>{
    //数据容器
    protected Object[] elementData;//new Object[100]{张三,李四,王五,null,null,...}
    //元素个数
    protected int elementCount;//3
    //容量增量
    protected int capacityIncrement;//50
    
    public Vector() {
        this(10);//利用无参构造创建Vector对象,底层默认容量为10
    }
    
    //initialCapacity - 10
    public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }
    
    //initialCapacity - 100
    //capacityIncrement - 50
    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }
    /*
    这段代码是Vector类中的add方法的实现。
	1. `synchronized`:使用`synchronized`关键字修饰方法(加锁),表示该方法是同步方法,可以确保在多线程环境下对该方法的访问是线程安全的。
	2. `modCount++`:增加外部操作数`modCount`,用于记录对Vector对象的修改操作。
	3. `ensureCapacityHelper(elementCount + 1)`:调用`ensureCapacityHelper`方法,确保Vector的容量足够来存放新的元素。`elementCount + 1`表示需要的最小容量。
	4. `elementData[elementCount++] = e`:将新的元素`e`添加到Vector的末尾。首先将新元素赋值给`elementData[elementCount]`,然后将`elementCount`的值加1,表示元素个数增加了一个。
	5. `return true`:返回`true`表示添加元素成功。
    */
    //e - 
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);//判断是否需要扩容
        elementData[elementCount++] = e;//先将e赋值给elementData[elementCount]后,elementCount+1
        return true;
    }
    
    //minCapacity - 101
    private void ensureCapacityHelper(int minCapacity) {
        //判断是否扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    /*
    这段代码是Vector类中的grow方法的实现。
	1. `int oldCapacity = elementData.length`:获取当前Vector的容量,即`elementData`数组的长度。
	2. `int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity)`:计算新的容量。如果容量增量`capacityIncrement`大于0,则新容量为旧容量加上容量增量;否则,新容量为旧容量的两倍。这样可以在扩容时,根据容量增量来决定扩容的大小。
	3. `if (newCapacity - minCapacity < 0) newCapacity = minCapacity`:检查新容量是否小于所需的最小容量`minCapacity`。如果是,则将新容量设为所需的最小容量。这样可以确保在扩容时,新容量至少能满足所需的最小容量。
	4. `if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity)`:检查新
    */
    //minCapacity - 101
    private void grow(int minCapacity) {
        // oldCapacity - 100
        int oldCapacity = elementData.length;
        // newCapacity - 100 + 50
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }
    
    public Enumeration<E> elements() {
        return new Enumeration<E>() {
            int count = 0;

            public boolean hasMoreElements() {
                return count < elementCount;
            }

            public E nextElement() {
                synchronized (Vector.this) {
                    if (count < elementCount) {
                        return elementData(count++);
                    }
                }
                throw new NoSuchElementException("Vector Enumeration");
            }
        };
    }
    
}
//Vector<String> v = new Vector<>();
		Vector<String> v = new Vector<>(100,50);
		
		v.add("张三");
		v.add("李四");
		v.add("王五");
		
		Enumeration<String> elements = v.elements();
		while(elements.hasMoreElements()){
			String element = elements.nextElement();
			System.out.println(element);
		}

ArrayList 和 Vector的区别

ArrayList 是JDK1.2才开始有的类,该集合不是线程安全,扩容机制是原来长度的1.5倍

Vector是JDK1.0就有的类,该集合是线程安全(加锁),扩容机制需要判断容量增量,容量增量为0,扩容机制就是原来长度的2倍,容量增量大于0,扩容机制就是原来长度+容量增量

HashMap底层

HashMap<Student, String> map = new HashMap<>();
		
		map.put(new Student("张三", '男', 21, "2301", "001"), "品茗");
		map.put(new Student("李四", '男', 23, "2301", "002"), "打篮球");
		map.put(new Student("王五", '男', 22, "2301", "003"), "听歌");
		
		map.put(new Student("王五", '男', 22, "2301", "003"), "美食");
		
		map.put(null, "玩游戏");
		map.put(null, "写代码");

2023年6月30日,线程,Vector底层,HashMap底层_Vector底层_05

这段代码是HashMap类的部分实现。

  1. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4:默认的初始容量,即哈希表的大小。这里使用位运算符<<将数字1左移4位,相当于将1乘以2的4次方,结果为16。
  2. static final int MAXIMUM_CAPACITY = 1 << 30:哈希表的最大容量。这里使用位运算符<<将数字1左移30位,相当于将1乘以2的30次方,结果为1073741824。
  3. static final float DEFAULT_LOAD_FACTOR = 0.75f:默认的负载因子。负载因子是指在哈希表中元素的数量与哈希表容量的比值。这里设置为0.75,表示当哈希表中元素数量达到容量的75%时,会触发扩容操作。
  4. static final Entry<?,?>[] EMPTY_TABLE = {}:空内容的数组,用于初始化哈希表的table数组。
  5. transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE:哈希表的数组容器,用于存储键值对的映射关系。初始时,将table数组设置为EMPTY_TABLE,即空数组。
  6. transient int size:元素个数,即哈希表中键值对的数量。
  7. int threshold:阈值,即容量与负载因子的乘积。当哈希表中元素数量达到阈值时,会触发扩容操作。
  8. final float loadFactor:负载因子。
  9. transient int modCount:外部操作数,用于记录哈希表被修改的次数。
  10. transient int hashSeed = 0:哈希种子数,用于引入随机性。
  11. public HashMap():HashMap类的无参构造方法。默认使用默认初始容量和默认负载因子来创建哈希表。
  12. public HashMap(int initialCapacity, float loadFactor):HashMap类的有参构造方法。根据指定的初始容量和负载因子来创建哈希表。
  13. static class Entry<K,V>:内部类Entry,表示哈希表中的一个节点或映射关系。每个Entry对象包含一个键值对的key和value,以及下一个节点的引用地址next和key的hash值hash。

HashMap是一种基于哈希表实现的键值对存储结构,它通过将键对象的哈希值映射到数组的下标位置来实现快速的插入、查找和删除操作。

在HashMap的源代码中,有一些重要的成员变量和方法:

  1. 成员变量
  • table:用于存储键值对的数组容器,具体的类型是Entry<K,V>[]
  • size:表示映射关系的个数,即存储在HashMap中的键值对数量。
  • threshold:表示数组容器的阈值,当size超过threshold时,会触发扩容操作。
  • loadFactor:表示负载因子,用于计算阈值,当size超过阈值时,会触发扩容操作。
  • modCount:表示外部操作数,用于进行快速失败检测。
  1. 方法
  • put:向HashMap中添加键值对。
  • get:根据键获取对应的值。
  • resize:扩容数组容器。
  • hash:计算键的哈希值。
  • indexFor:根据哈希值和数组长度计算键的存储位置。

在HashMap的实现中,哈希碰撞是一个重要的问题。当不同的键对象具有相同的哈希值时,它们会被存储在数组的同一个位置上,形成一个链表结构。为了提高性能,HashMap会在链表长度达到一定阈值时,将链表转化为红黑树结构。

需要注意的是,HashMap是非线程安全的,如果多个线程同时访问HashMap并进行修改操作,可能会导致数据不一致的问题。如果需要在多线程环境下使用HashMap,可以考虑使用ConcurrentHashMap或者使用适当的同步机制来保证线程安全性。

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{
    
    //默认容量 - 容量必须的2的幂
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
	//最大容量 -- int类型取值范围内最大的2的幂的数字
    static final int MAXIMUM_CAPACITY = 1 << 30;//1073741824
	//默认的负载因子
    //1 --》 16*1=16(阈值) 	 -- 扩容数组慢(利用了空间,牺牲时间)
    //0.1 --》 16*0.1=1(阈值) -- 扩容数组快(利用了时间,牺牲了空间)
    //0.75 --> 16*0.75=12(阈值) -- 装12个数据就扩容(取得时间和空间的平衡)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
	//空内容的数组
    static final Entry<?,?>[] EMPTY_TABLE = {};
	//数组容器 -- hash表、hash数组
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//new Entry[16]{}
	//元素个数(映射关系的个数)
    transient int size;//0
	//阈值(容量*负载因子)
    int threshold;//12
	//负载因子
    final float loadFactor;//0.75
	//外部操作数
    transient int modCount;//0
    //hash种子数
    /*
    在HashMap类中,`transient int hashSeed = 0`是一个表示哈希种子数的成员变量。关键字`transient`表示该变量不会被序列化,即不会被保存到持久化存储介质中,例如磁盘或网络传输。
	哈希种子数是用于引入随机性的常数值,用于初始化哈希函数的内部状态。它的作用是在哈希计算过程中增加随机性,以避免出现哈希冲突。通过引入随机性,可以增加哈希函数的安全性和减少攻击者对哈希函数的预测性。
	在HashMap类中,默认的哈希种子数为0,表示不使用哈希种子数。这意味着在相同的输入值上进行哈希计算时,会得到相同的哈希值。如果需要增加哈希函数的随机性,可以通过修改hashSeed的值来改变哈希种子数。
	需要注意的是,哈希种子数一般是固定的,不会随着时间或程序执行而改变。这是为了保证对相同的输入值进行哈希时,能够得到相同的哈希值,从而保持数据结构的一致性和可预测性。
    */
    transient int hashSeed = 0;//0
    
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }
    
    //initialCapacity - 16
    //loadFactor - 0.75
    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))//NaN - Not a Number
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
        init();
    }
    
    //toSize - 16
    private void inflateTable(int toSize) {
        //计算容量(获取toSize的二的幂的数字) -- 16
        int capacity = roundUpToPowerOf2(toSize);

        //计算阈值 - 12
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //初始化容器数组 -- new Entry[16]
        table = new Entry[capacity];
        //计算hash种子数
        initHashSeedAsNeeded(capacity);
    }
    
    //number - 16
    private static int roundUpToPowerOf2(int number) {
		//Integer.highestOneBit(数字) -- 保留最高位的1,其余都是0
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number-1) << 1) : 1;
    }
    //putForNullKey(V value) 方法的作用是处理键为 null 的情况下的插入操作。它会遍历容器数组中下标为 	0 的位置的链表,如果发生哈希碰撞则更新节点的值并返回原来节点的值,如果没有发生哈希碰撞则将新的键值对添	加到链表的头部。
    private V putForNullKey(V value) {
        //下标为0的位置有Entry对象,就意味着hash碰撞了
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            //判断Key值是否为0,若将hash值改为0,第0个下标的位置会存储key不为空的Entry对象
            if (e.key == null) {
                //获取老的value
                V oldValue = e.value;
                //替换value
                e.value = value;
                e.recordAccess(this);
                return oldValue;//返回老的value
            }
        }
        modCount++;
        addEntry(0, null, value, 0);//把数据添加到Entry对象中,Entry对象添加table中
        return null;
    }
    //hash(Object k) 方法的作用是计算给定键 k 的哈希值。它先判断键 k 是否是 String 类型,如果是则调		用 sun.misc.Hashing.stringHash32(String str) 方法计算字符串的哈希值,否则将键 k 的哈希值与局	部变量 h 进行混合运算,最终得到最终的哈希值。
    final int hash(Object k) {
        int h = hashSeed;
        //判断key是否是String,如果是,就计算hash值(hashSeed去参与计算hash值的工作)
        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);
    }
    //indexFor(int h, int length) 方法的作用是计算给定哈希值 h 在容器数组中的索引位置。它通过将哈希	值 h 与容器数组长度减 1 进行按位与运算,得到一个在容器数组下标范围内的索引值。这样可以保证元素在容器数	组中的散列均匀,减少哈希碰撞的几率,提高效率。
    static int indexFor(int h, int length) {
        //长度必须的2的幂,是因为要让元素散列均匀
        //如果长度不为2的幂,会增加同一个下标上有多个元素的几率(hash碰撞),导致效率降低
        return h & (length-1);
    }
    
    //key - new Student("王五", '男', 22, "2301", "003")
    //value - "美食"
    public V put(K key, V value) {
        //第一次添加元素时,进入的判断
        if (table == EMPTY_TABLE) {
            //初始化数据(阈值、hash种子、数组)
            inflateTable(threshold);
        }
        //判断key是否是null,如果是null,就将数据添加至数组下标为0的位置
        if (key == null)
            return putForNullKey(value);
        //获取key的hash值
        int hash = hash(key);
        //利用hash值计算在数组中的下标
        int i = indexFor(hash, table.length);
        //判断下标上是否有Entry对象
        //进入该判断,就意味着hash碰撞(竟可能的避免 -- 重写key的hashCode和equals)
        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))) {
               	//获取老的value
                V oldValue = e.value;
                //替换value值
                e.value = value;
                e.recordAccess(this);
                //返回老的value
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }
    
    //hash - 20
    //key - new Student("李四", '男', 23, "2301", "002")
    //value - "打篮球"
    //bucketIndex - 4
    //addEntry(int hash, K key, V value, int bucketIndex) 方法的作用是向 HashMap 中添加新的键	值对。它首先判断是否需要进行扩容,如果需要则进行扩容操作。然后,调用 createEntry(int hash, K key, 	V value, int bucketIndex) 方法创建新的 Entry,并将其插入到指定的索引位置上。
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //判断是否扩容
        /*在 `addEntry(int hash, K key, V value, int bucketIndex)` 方法中,用于判断是否需要进行扩容操作。
        1. 首先,判断当前 HashMap 的 size(元素个数)是否大于等于 threshold(容量阈值)。如果满足这个条件,说明 HashMap 已经达到了扩容的条件,需要进行扩容操作
		2. 接着,判断指定索引位置 bucketIndex 上是否已经存在元素(即 table[bucketIndex] 不为空)。如果指定索引位置上已经存在元素,说明该位置已经发生了哈希碰撞(即多个键映射到了同一个索引位置上)。在发生哈希碰撞的情况下,需要进行扩容操作。
		3. 如果满足以上两个条件,就调用 `resize(int newCapacity)` 方法对 HashMap 进行扩容操作。扩容后,HashMap 的容量会变为原来的两倍。
		4. 扩容操作完成后,重新计算新的哈希值 hash 和索引位置 bucketIndex。如果 key 不为空,就使用 `hash(key)` 方法重新计算哈希值;否则,将哈希值设置为 0。然后,使用 `indexFor(hash, table.length)` 方法重新计算索引位置。
		这段代码的作用是在向 HashMap 添加新的键值对之前,判断是否需要进行扩容操作。如果当前 	HashMap 的 size 大于等于 threshold,并且指定索引位置上已经存在元素,就进行扩容操作。扩容后,重新计算新的哈希值和索引位置。这样可以保证 HashMap 的负载因子在扩容后仍然处于合理的范围内,提高 HashMap 的性能。
		*/
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }
    
    //hash - 20
    //key - new Student("李四", '男', 23, "2301", "002")
    //value - "打篮球"
    //bucketIndex - 4
    void createEntry(int hash, K key, V value, in t bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
    
    //节点类/映射关系类
    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key; ------ key
        V value; ---------- value
        Entry<K,V> next; -- 下一个节点的引用地址
        int hash; --------- key的hash值
            
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
    }
    
}
HashMap<Student, String> map = new HashMap<>();
		
		map.put(new Student("张三", '男', 21, "2301", "001"), "品茗");
		map.put(new Student("李四", '男', 23, "2301", "002"), "打篮球");
		map.put(new Student("王五", '男', 22, "2301", "003"), "听歌");
		
		map.put(new Student("王五", '男', 22, "2301", "003"), "美食");
		
		map.put(null, "玩游戏");
		map.put(null, "写代码");

学习HashMap的过程:

  1. 创建对象的过程(注意:属性的初始化)
  2. 添加数据的过程(注意:添加的步骤、hash碰撞)
  3. 扩容的过程(注意:hash回环)

在哈希表的扩容操作中,可能会发生哈希回环(Hash Collision)。当哈希表需要扩容时,它会创建一个更大的数组,并将原来的键值对重新哈希到新的数组中。

在重新哈希的过程中,由于新数组的大小变大了,哈希函数的计算结果也会发生变化。但是,由于哈希函数的输出空间通常比键的数量要小,因此仍然会有不同的键通过哈希函数计算得到相同的哈希值,导致哈希回环的发生。

当发生哈希回环时,哈希表会使用链表来解决冲突。即使多个键映射到同一个索引位置上,它们仍然可以按照插入的顺序以链表的形式存储在该位置上。在扩容操作中,原来的链表会被拆分成多个链表,分别重新哈希到新的数组中的不同位置上。

需要注意的是,扩容操作中的哈希回环是临时的,只在重新哈希的过程中发生。一旦扩容完成,哈希表的容量增大,并且通过新的哈希函数计算得到的哈希值分布更加均匀,减少了哈希回环的发生概率。

总结起来,哈希表在扩容操作中可能会发生哈希回环。在重新哈希的过程中,不同的键可能会通过哈希函数计算得到相同的哈希值,导致冲突发生。哈希表使用链表来解决哈希回环,将相同哈希值的键值对存储在同一个链表中。扩容操作会重新哈希键值对,并将它们分散到新的数组中的不同位置上,减少哈希回环的发生概率。