线程
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]);
}
}
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();
}
}
}
}
4. 线程的中断
在Java中,线程的中断是通过调用线程的interrupt()
方法来触发的。当一个线程被中断时,它的中断状态会被设置为"中断"。
可以通过Thread.currentThread().isInterrupted()
方法来查询当前线程的中断状态,或者通过Thread.interrupted()
方法来检查当前线程的中断状态,并且清除中断状态,以便下一次中断能够被正确地检测到。
以下是一些常见的线程中断的应用场景和用法:
- 终止循环:在任务的主循环中,可以通过检查中断状态来决定是否继续执行任务。例如:
while (!Thread.currentThread().isInterrupted()) {
// 执行任务
//Thread.currentThread().isInterrupted()判断当前线程是否销毁,销毁是true,未销毁是false,注意:置反
}
- 响应中断:在执行阻塞操作时,如果线程被中断,可以通过捕获
InterruptedException
异常来响应中断,并进行相应的处理。例如:
try {
while (!Thread.currentThread().isInterrupted()) {
// 执行可中断的阻塞操作,如Thread.sleep(), Object.wait(), Lock.lockInterruptibly()等
}
} catch (InterruptedException e) {
// 响应中断
}
- 中断其他线程:可以通过调用其他线程对象的
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);
}
}
}
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()方法执行结束后,线程即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
Vector底层
public interface Enumeration<E> {
//判断是否有下一个可迭代的元素
boolean hasMoreElements();
//获取下一个元素
E nextElement();
}
Vector类的实现。
AbstractList<E>
:抽象类,实现了List接口,并继承了AbstractCollection抽象类。其中,modCount
用于记录外部操作数。Vector<E>
:实现了List接口,继承了AbstractList抽象类。实现了一个可变大小的数组。elementData
:数据容器,用于存储元素的数组。elementCount
:元素的个数。capacityIncrement
:容量增量,用于确定在需要增加容量时,容量的增加量。Vector()
:无参构造方法,创建一个初始容量为10的Vector对象。Vector(int initialCapacity)
:带有初始容量参数的构造方法,创建一个指定初始容量的Vector对象。Vector(int initialCapacity, int capacityIncrement)
:带有初始容量和容量增量参数的构造方法,创建一个指定初始容量和容量增量的Vector对象。add(E e)
:向Vector末尾添加一个元素。首先增加外部操作数modCount
,然后通过ensureCapacityHelper
方法确保容量足够,最后将元素添加到elementData
数组的末尾。ensureCapacityHelper(int minCapacity)
:检查是否需要扩容。如果需要扩容,则调用grow
方法进行扩容。grow(int minCapacity)
:扩容数组。根据容量增量和当前容量计算新的容量,然后使用Arrays.copyOf
方法将原数组的元素复制到新数组中。elementData(int index)
:根据索引获取元素。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, "写代码");
这段代码是HashMap类的部分实现。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
:默认的初始容量,即哈希表的大小。这里使用位运算符<<
将数字1左移4位,相当于将1乘以2的4次方,结果为16。static final int MAXIMUM_CAPACITY = 1 << 30
:哈希表的最大容量。这里使用位运算符<<
将数字1左移30位,相当于将1乘以2的30次方,结果为1073741824。static final float DEFAULT_LOAD_FACTOR = 0.75f
:默认的负载因子。负载因子是指在哈希表中元素的数量与哈希表容量的比值。这里设置为0.75,表示当哈希表中元素数量达到容量的75%时,会触发扩容操作。static final Entry<?,?>[] EMPTY_TABLE = {}
:空内容的数组,用于初始化哈希表的table数组。transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE
:哈希表的数组容器,用于存储键值对的映射关系。初始时,将table数组设置为EMPTY_TABLE,即空数组。transient int size
:元素个数,即哈希表中键值对的数量。int threshold
:阈值,即容量与负载因子的乘积。当哈希表中元素数量达到阈值时,会触发扩容操作。final float loadFactor
:负载因子。transient int modCount
:外部操作数,用于记录哈希表被修改的次数。transient int hashSeed = 0
:哈希种子数,用于引入随机性。public HashMap()
:HashMap类的无参构造方法。默认使用默认初始容量和默认负载因子来创建哈希表。public HashMap(int initialCapacity, float loadFactor)
:HashMap类的有参构造方法。根据指定的初始容量和负载因子来创建哈希表。static class Entry<K,V>
:内部类Entry,表示哈希表中的一个节点或映射关系。每个Entry对象包含一个键值对的key和value,以及下一个节点的引用地址next和key的hash值hash。HashMap是一种基于哈希表实现的键值对存储结构,它通过将键对象的哈希值映射到数组的下标位置来实现快速的插入、查找和删除操作。
在HashMap的源代码中,有一些重要的成员变量和方法:
- 成员变量
table
:用于存储键值对的数组容器,具体的类型是Entry<K,V>[]
。size
:表示映射关系的个数,即存储在HashMap中的键值对数量。threshold
:表示数组容器的阈值,当size
超过threshold
时,会触发扩容操作。loadFactor
:表示负载因子,用于计算阈值,当size
超过阈值时,会触发扩容操作。modCount
:表示外部操作数,用于进行快速失败检测。
- 方法
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的过程:
- 创建对象的过程(注意:属性的初始化)
- 添加数据的过程(注意:添加的步骤、hash碰撞)
- 扩容的过程(注意:hash回环)
在哈希表的扩容操作中,可能会发生哈希回环(Hash Collision)。当哈希表需要扩容时,它会创建一个更大的数组,并将原来的键值对重新哈希到新的数组中。
在重新哈希的过程中,由于新数组的大小变大了,哈希函数的计算结果也会发生变化。但是,由于哈希函数的输出空间通常比键的数量要小,因此仍然会有不同的键通过哈希函数计算得到相同的哈希值,导致哈希回环的发生。
当发生哈希回环时,哈希表会使用链表来解决冲突。即使多个键映射到同一个索引位置上,它们仍然可以按照插入的顺序以链表的形式存储在该位置上。在扩容操作中,原来的链表会被拆分成多个链表,分别重新哈希到新的数组中的不同位置上。
需要注意的是,扩容操作中的哈希回环是临时的,只在重新哈希的过程中发生。一旦扩容完成,哈希表的容量增大,并且通过新的哈希函数计算得到的哈希值分布更加均匀,减少了哈希回环的发生概率。
总结起来,哈希表在扩容操作中可能会发生哈希回环。在重新哈希的过程中,不同的键可能会通过哈希函数计算得到相同的哈希值,导致冲突发生。哈希表使用链表来解决哈希回环,将相同哈希值的键值对存储在同一个链表中。扩容操作会重新哈希键值对,并将它们分散到新的数组中的不同位置上,减少哈希回环的发生概率。