一个 HashMap 内部有一个容器数组,用来存放具体的数据,每一次调用 put
方法存入数据时,先对数据的 key
调用 hash 函数得到一个 hash 值,再将其与容器数组长度取余得到一个索引值,将该数据存放在数组对应索引的位置,之后调用 get
方法读取数据时,同样取 key
的 hash 值得到在数组中的位置,直接从数组中读取元素,查找的时间复杂度为
因为容器数组长度有限,存入大量数据时可能会产生 hash 碰撞,即不同的元素可能被存放在数组的同一位置,采用拉链法解决即将容器数组中的每一个容器实现为一个链表,则每次查找元素需要先定位到在容器数组中的位置,再在容器链表中找到对应元素。当链表上有大量数据时,查找效率会变得低下,当链表长度达到一定值时,将链表转为适合查找的二叉查找树结构。
当数据量更大的时候,出现 hash 碰撞的可能性会更大,需要对容器数组进行扩容,每次都扩大为原来的两倍。扩容后由于容器数组的长度发生改变,需要将原来的数据全部重新放入。
此外,因为允许将 null
作为 key
值放入,对空值需要进行一些判定;当有 key
值重复的元素放入时,直接将原来的元素覆盖。
对于单链表查询,时间复杂度为
- 根节点为空,直接将待插入节点作为根节点
- 比较待插入节点与根节点的大小,小(大)于根节点时,若根节点左(右)子结点为空,则直接将待插入结点插入,否则将左(右)子结点作为根节点递归插入
- 待删除节点没有子节点,则直接将该节点删除即可
- 待删除节点仅有左(右)子节点,则将其左(右)子节点代替待删除节点即可
- 待删除节点有左、右子节点,则找到其右子树的最小节点,将其替换为待删除节点即可
有时候多个线程可能会对同一个共享变量进行操作,JVM 里变量的值保存在主内存中,而每个线程访问变量时,获取到的是一个变量的副本,线程在修改了变量后会在一个不确定的时间将修改后的变量值写入回主内存,就可能造成多个线程共享的变量数据不一致。
在声明变量时使用 volatile
关键字修饰,使得该变量在被读写时总是能从主内存获取最新值或立刻写入主内存。其解决了可见性的问题,当一个线程修改了共享变量的值后,其他的线程能够立即访问到最新的值。
当多个线程同时允许时,线程的调度由操作系统决定,如果多个线程同时改变一个共享变量,进行的操作并非原子操作(不可中断的一个或一系列操作,除 long
和 double
的基本类型以及引用类型的赋值操作),系统在调度时可能一个线程并未完成操作便被中断,也可能导致共享变量的数据不一致的情况。
则我们需要当一个线程在对一个变量进行一系列操作的时候,其它线程必须等待正在执行的线程操作完成,需要通过对相应代码块加锁。
使用 synchronized
关键字,可以保证某段代码在任一时刻最多只能有一个线程在执行。其中的 lock
是用作锁的对象实例,在代码块执行结束后会自动释放锁。
synchronized(lock) {
// do something...
}
若在方法前用 synchronized
修饰,则将以当前对象实例为锁
public synchronized someMethod() {
// do something...
}
public someMethod() {
synchronized(this) {
// do something....
}
}
JDK 中提供了一个线程安全的 ConcurrentHashMap
,其实现思路大概是其内部多了一个 Segment[]
数组,每一个 Segment
对象就像是一个小的 HashMap
,里面也包含了一个存放 Entry
键值对的数组,同样能形成链表结构。然后每个 Segment
对象各自持有一把锁,每次写入时,先需要通过 hash 定位到 Segment
,然后再次通过 hash 定位到 Segment
中数组的具体位置,这样在读写不同的 Segment
中的数据时可以实现并发完成。
- Part D Test 数据量过大时 HashMap 中会多出几个元素,偶尔又正常,debug 发现问题大概率出现在链表转换为二叉搜索树的时候,目前暂未解决(