首页 文章 精选 留言 我的

精选列表

搜索[提高],共10000篇文章
优秀的个人博客,低调大师

探究微软设置CPU限制和提高运行门槛的动机

在 Windows 11 首个预览版发布之后,虽然微软对 CPU 升级限制和最低设备要求进行了澄清,但依然存在模糊点。微软表示只有第 8 代英特尔芯片、Ryzen 2000 系列以上芯片才能升级 Windows 11 系统。那么之前的设备难道真的就无法运行 Windows 11 了吗?事实可能并非如此。 首先,从此前偷跑的 ISO 镜像以及当前 Windows Insider 分发的 Build 22000.51 镜像,Windows 11 在第 7 代处理器和更低的设备上也运行良好。当从 ISO 镜像安装的时候,Windows 11 绕过 TPM 2.0 限制是非常简单的,只需将一个.dll替换成Windows 10中的一个。根据Albacore在Twitter上的说法,如果你进行清洁安装,Windows 11 甚至不检查 CPU 世代。 对于那些想知道纯净安装 Windows 11 的最低硬件要求是什么(当你从USB启动安装时) 3686MB内存 2个CPU核心 1 GHz基本速度 TPM 1.2 支持 Secure Boot 的固件 如果你是在虚拟机或部署服务器,所有的检查都会被跳过。纯净安装的时候不会检查 CPU。 那么微软为何要对 CPU 代数进行限制? 那么微软为何要这么麻烦呢?首先,CPU代数限制与TPM完全无关,它是为了 "经验原因",正如操作系统安全总监大卫·韦斯顿所指出的:“CPU 是出于经验的原因。和 TPM 的要求是不相关的”。 在过去,只要他们的电脑符合最低规格(大多数第四代、第五代、第六代和第七代设备都符合,只要它们以某种方式启用 TPM),就可以由用户决定他们是否想要一个“伟大的体验”。对于用户来说,如果因为他们完美的第六代设备不符合 CPU 限制而不得不花 1500 美元以上购买一台新机器,这真的是一种“伟大的体验”吗? 那么为何微软从一开始就引起了这些不安,外媒推测原因可能有两个 a:让很多人感到不安,然后升级设备 最被消费者接纳的说法就是微软想迫使用户购买新机器。当然,这对OEM厂商和微软来说都不是一件坏事,但如果有的话,Windows 11可以让人们在Windows 10上停留更长时间。 b:为了运行 Android 应用程序或者其他后续功能 第二种可能涉及到 HVCI,这是一个管理程序的安全措施。它可能会影响到像 Windows Subsystem for Android 这样的东西,它被用来在 Windows 11 上运行 Android 应用程序。如果 Android 应用在旧版 CPU 上表现不佳,微软确实可能非常不愿意允许出现不合格的体验。 而另一个可能性是,微软仍对 Windows 11 有后续的发展计划,这些计划目前并未被披露,需要更现代的 CPU 来运行。从事Azure工作的微软资深人士Carmen Crincoli(他在Twitter上明确表示,他 "代表(他)自己,而不是微软")在Twitter上说,“他的目标是创造一条前进的道路,使生态系统在几年后处于更强大的地位”。 如果ARM上的Windows和Android的Windows子系统、HVCI,以及Windows 11和Windows 10之间的密切关系是这些硬件限制的原因,为什么不直接说出来?即使有最好的意图,有时微软也无法摆脱自己的方式。 【责任编辑:未丽燕 TEL:(010)68476606】

优秀的个人博客,低调大师

中断 Hwi:提高鸿蒙轻内核系统实时性及执行效率的秘密武器

摘要:本文带领大家一起剖析了鸿蒙轻内核的中断模块的源代码,掌握中断相关的概念,中断初始化操作,中断创建、删除,开关中断操作等。 本文分享自华为云社区《鸿蒙轻内核M核源码分析系列五 中断Hwi》,原文作者:zhushy。 本文,我们讲述一下中断,会给读者介绍中断的概念,鸿蒙轻内核的中断模块的源代码。本文中所涉及的源码,以 OpenHarmony LiteOS-M 内核为例,均可以在开源站点gitee.com/openharmony… 获取。 1、中断概念介绍 中断是指出现需要时,CPU 暂停执行当前程序,转而执行新程序的过程。当外设需要 CPU 时,将通过产生中断信号使 CPU 立即中断当前任务来响应中断请求。在剖析中断源代码之前,下面介绍些中断相关的硬件、中断相关的概念。 1.1 中断相关的硬件介绍 与中断相关的硬件可以划分为三类:设备、中断控制器、CPU 本身。 设备 发起中断的源,当设备需要请求 CPU 时,产生一个中断信号,该信号连接至中断控制器。 中断控制器 中断控制器是 CPU 众多外设中的一个,它一方面接收其它外设中断引脚的输入。另一方面,它会发出中断信号给 CPU。可以通过对中断控制器编程来打开和关闭中断源、设置中断源的优先级和触发方式。 CPU CPU 会响应中断源的请求,中断当前正在执行的任务,转而执行中断处理程序。 1.2 中断相关的概念 中断号 每个中断请求信号都会有特定的标志,使得计算机能够判断是哪个设备提出的中断请求,这个标志就是中断号。 中断优先级 为使系统能够及时响应并处理所有中断,系统根据中断时间的重要性和紧迫程度,将中断源分为若干个级别,称作中断优先级。 中断处理程序 当外设产生中断请求后,CPU 暂停当前的任务,转而响应中断申请,即执行中断处理程序。产生中断的每个设备都有相应的中断处理程序。 中断向量 中断服务程序的入口地址。 中断向量表 存储中断向量的存储区,中断向量与中断号对应,中断向量在中断向量表中按照中断号顺序存储。 中断共享 当外设较少时,可以实现一个外设对应一个中断号,但为了支持更多的硬件设备,可以让多个设备共享一个中断号,共享同一个中断号的中断处理程序形成一个链表。当外部设备产生中断申请时,系统会遍历中断号对应的中断处理程序链表,直到找到对应设备的中断处理程序。在遍历执行过程中,各中断处理程序可以通过检测设备 ID,判断是否是这个中断处理程序对应的设备产生的中断。 接下来,我们再看看鸿蒙轻内核中断源代码。 2、鸿蒙轻内核中断源代码 2.1 中断相关的声明和定义 在文件 kernel\arch\arm\cortex-m7\gcc\los_interrupt.c 中定义了一些结构体、全局变量、内联函数,在分析源码之前,我们先看下这些定义和声明。全部变量 g_intCount 表示正在处理的中断数量,每次进入中断处理程序时,都会把该变量数值加 1,完成中断处理退出时,该数值减 1。对应的内联函数 HalIsIntActive()用于获取是否正在处理中断,返回值大于 0,则表示正在处理中断。 UINT32g_intCount=0; inlineUINT32HalIsIntActive(VOID) { return(g_intCount>0); } 我们在再看看中断向量表定义。⑴处代码为系统支持的中断定义了数组 g_hwiForm[OS_VECTOR_CNT],对于每一个中断号 hwiNum,对应的数组元素 g_hwiForm[hwiNum]表示每一个中断对应的中断处理执行入口程序。⑵处的宏 OS_HWI_WITH_ARG 表示中断处理程序是否支持参数传入,默认关闭。如果支持传参,定义⑶处的结构体 HWI_HANDLER_FUNC 来维护中断处理函数及其参数,还需要定义⑷处 g_hwiHandlerForm 数组。如果不支持传参,使用⑹处定义的 g_hwiHandlerForm 数组。对于每一个中断号 hwiNum,对应的数组元素 g_hwiHandlerForm[hwiNum]表示每一个中断对应的中断处理程序。⑸、⑺处定义个函数 OsSetVector()用于设置指定中断号对应的中断处理执行入口程序和中断处理程序。中断处理执行入口程序和中断处理程序的关系是,当中断发生时,会执行中断处理执行入口程序,这个函数会进一步调用中断处理程序。 ⑴STATICHWI_PROC_FUNC__attribute__((aligned(0x100)))g_hwiForm[OS_VECTOR_CNT]={0}; ⑵#if(OS_HWI_WITH_ARG==1) ⑶typedefstruct{ HWI_PROC_FUNCpfnHandler; VOID*pParm; }HWI_HANDLER_FUNC; ⑷STATICHWI_HANDLER_FUNCg_hwiHandlerForm[OS_VECTOR_CNT]={{(HWI_PROC_FUNC)0,(HWI_ARG_T)0}}; ⑸VOIDOsSetVector(UINT32num,HWI_PROC_FUNCvector,VOID*arg) { if((num+OS_SYS_VECTOR_CNT)<OS_VECTOR_CNT){ g_hwiForm[num+OS_SYS_VECTOR_CNT]=(HWI_PROC_FUNC)HalInterrupt; g_hwiHandlerForm[num+OS_SYS_VECTOR_CNT].pfnHandler=vector; g_hwiHandlerForm[num+OS_SYS_VECTOR_CNT].pParm=arg; } } #else ⑹STATICHWI_PROC_FUNCg_hwiHandlerForm[OS_VECTOR_CNT]={0}; ⑺VOIDOsSetVector(UINT32num,HWI_PROC_FUNCvector) { if((num+OS_SYS_VECTOR_CNT)<OS_VECTOR_CNT){ g_hwiForm[num+OS_SYS_VECTOR_CNT]=HalInterrupt; g_hwiHandlerForm[num+OS_SYS_VECTOR_CNT]=vector; } } #endif 2.2 中断初始化 HalHwiInit() 在系统启动时,在 kernel\src\los_init.c 中调用 HalArchInit()进行中断初始化。这个函数定义在 kernel\arch\arm\cortex-m7\gcc\los_context.c,然后进一步调用定义在 kernel\arch\arm\cortex-m7\gcc\los_interrupt.c 文件中 HalHwiInit()函数完成中断向量初始化。我们分析下代码。 宏LOSCFG_USE_SYSTEM_DEFINED_INTERRUPT 表示是否使用系统预定义的向量基地址和中断处理程序,默认开启。⑴处开始,中断向量表的 0 号中断设置为空,1 号中断对应复位处理程序 Reset_Handler。⑵处把其余的中断设置为默认的中断处理执行入口程序 HalHwiDefaultHandler()。⑶处设置系统中断(异常是中断的一种,系统中断也称为异常),系统中断的执行入口函数定义在 kernel\arch\arm\cortex-m7\gcc\los_exc.S,使用汇编语言实现。系统中断中,14 号中断对应 HalPendSV 处理程序,用于任务上下文切换,15 号中断是 tick 中断。 执行⑷处代码把中断向量表赋值给 SCB->VTOR。对于 Cortex-M3 及以上的 CPU 核,还需要执行⑸设置优先级组。⑹处代码使能指定的异常。 LITE_OS_SEC_TEXT_INITVOIDHalHwiInit() { #if(LOSCFG_USE_SYSTEM_DEFINED_INTERRUPT==1) UINT32index; ⑴g_hwiForm[0]=0;/*[0]TopofStack*/ g_hwiForm[1]=Reset_Handler;/*[1]reset*/ ⑵for(index=2;index<OS_VECTOR_CNT;index++){/*2:Thestartingpositionoftheinterrupt*/ g_hwiForm[index]=(HWI_PROC_FUNC)HalHwiDefaultHandler; } /*Exceptionhandlerregister*/ ⑶g_hwiForm[NonMaskableInt_IRQn+OS_SYS_VECTOR_CNT]=HalExcNMI; g_hwiForm[HARDFAULT_IRQN+OS_SYS_VECTOR_CNT]=HalExcHardFault; g_hwiForm[MemoryManagement_IRQn+OS_SYS_VECTOR_CNT]=HalExcMemFault; g_hwiForm[BusFault_IRQn+OS_SYS_VECTOR_CNT]=HalExcBusFault; g_hwiForm[UsageFault_IRQn+OS_SYS_VECTOR_CNT]=HalExcUsageFault; g_hwiForm[SVCall_IRQn+OS_SYS_VECTOR_CNT]=HalExcSvcCall; g_hwiForm[PendSV_IRQn+OS_SYS_VECTOR_CNT]=HalPendSV; g_hwiForm[SysTick_IRQn+OS_SYS_VECTOR_CNT]=SysTick_Handler; /*Interruptvectortablelocation*/ ⑷SCB->VTOR=(UINT32)(UINTPTR)g_hwiForm; #endif #if(__CORTEX_M>=0x03U)/*onlyforCortex-M3andabove*/ ⑸NVIC_SetPriorityGrouping(OS_NVIC_AIRCR_PRIGROUP); #endif /*EnableUSGFAULT,BUSFAULT,MEMFAULT*/ ⑹*(volatileUINT32*)OS_NVIC_SHCSR|=(USGFAULT|BUSFAULT|MEMFAULT); /*EnableDIV0andunalignedexception*/ *(volatileUINT32*)OS_NVIC_CCR|=DIV0FAULT; return; } 2.3 创建中断 UINT32 HalHwiCreate() 开发者可以调用函数 UINT32HalHwiCreate()创建中断,注册中断处理程序。我们先看看这个函数的参数,HWI_HANDLE_T hwiNum 是硬件中断号,HWI_PRIOR_ThwiPrio 中断的优先级,HWI_MODE_T mode 中断模式,保留暂时没有使用。HWI_PROC_FUNC handler 是需要注册的中断处理程序,中断被触发后会调用这个函数。HWI_ARG_T arg 是中断处理程序的参数。 一起剖析下这个函数的源代码,⑴处代码开始,对入参进行校验,中断处理程序不能为空,中断号不能大于支持的最大中断号,中断优先级不能超过指定优先级的大小。如果待创建的中断号对应的中断执行入口程序不等于 HalHwiDefaultHandler,说明已经创建过,返回错误码。关中断,然后执行⑵处的 OsSetVector()函数设置指定中断号的中断处理程序。⑶处调用 CMSIS 函数使能中断、设置中断的优先级,打开中断,完成中断的创建。 LITE_OS_SEC_TEXT_INITUINT32HalHwiCreate(HWI_HANDLE_ThwiNum, HWI_PRIOR_ThwiPrio, HWI_MODE_Tmode, HWI_PROC_FUNChandler, HWI_ARG_Targ) { UINTPTRintSave; ⑴if(handler==NULL){ returnOS_ERRNO_HWI_PROC_FUNC_NULL; } if(hwiNum>=OS_HWI_MAX_NUM){ returnOS_ERRNO_HWI_NUM_INVALID; } if(g_hwiForm[hwiNum+OS_SYS_VECTOR_CNT]!=(HWI_PROC_FUNC)HalHwiDefaultHandler){ returnOS_ERRNO_HWI_ALREADY_CREATED; } if(hwiPrio>OS_HWI_PRIO_LOWEST){ returnOS_ERRNO_HWI_PRIO_INVALID; } intSave=LOS_IntLock(); #if(OS_HWI_WITH_ARG==1) OsSetVector(hwiNum,handler,arg); #else ⑵OsSetVector(hwiNum,handler); #endif ⑶NVIC_EnableIRQ((IRQn_Type)hwiNum); NVIC_SetPriority((IRQn_Type)hwiNum,hwiPrio); LOS_IntRestore(intSave); returnLOS_OK; } 2.4 删除中断 UINT32 HalHwiDelete() 中断删除操作是创建操作的反向操作,也比较好理解。开发者可以调用函数 UINT32 HalHwiDelete(HWI_HANDLE_T hwiNum)来删除中断。函数需要指定中断号参数 HWI_HANDLE_T hwiNum。一起剖析下这个函数的源代码,⑴处代码对入参进行校验,不能大于支持的最大中断号。⑵处调用 CMSIS 函数来失能中断,然后锁中断,执行⑶把中断向量表指定中断号的中断执行入口程序设置为默认程序 HalHwiDefaultHandler。 LITE_OS_SEC_TEXT_INITUINT32HalHwiDelete(HWI_HANDLE_ThwiNum) { UINT32intSave; ⑴if(hwiNum>=OS_HWI_MAX_NUM){ returnOS_ERRNO_HWI_NUM_INVALID; } ⑵NVIC_DisableIRQ((IRQn_Type)hwiNum); intSave=LOS_IntLock(); ⑶g_hwiForm[hwiNum+OS_SYS_VECTOR_CNT]=(HWI_PROC_FUNC)HalHwiDefaultHandler; LOS_IntRestore(intSave); returnLOS_OK; } 2.5 中断处理执行入口程序 我们再来看看中断处理执行入口程序。默认的函数 HalHwiDefaultHandler()如下,调用函数 HalIntNumGet()获取中断号,打印输出,然后进行死循环。其中函数 HalIntNumGet()读取寄存器 ipsr 来获取触发的中断的中断号。 LITE_OS_SEC_TEXT_MINORVOIDHalHwiDefaultHandler(VOID) { UINT32irqNum=HalIntNumGet(); PRINT_ERR("%sirqNum:%d\n",__FUNCTION__,irqNum); while(1){} } 继续来看中断处理执行入口程序 HalInterrupt(),源码如下。 ⑴处把全局变量 g_intCount 表示的正在处理的中断数量加 1,在中断执行完毕后,在⑹处再把正在处理的中断数量减 1。⑵处调用函数 HalIntNumGet()获取中断号,⑶、⑸处调用的函数 HalPreInterruptHandler(),HalAftInterruptHandler()在执行中断处理程序前、后可以处理些其他操作,当前默认为空函数。⑷处根据中断号从中断处理程序数组中获取中断处理程序,不为空就调用执行。 LITE_OS_SEC_TEXTVOIDHalInterrupt(VOID) { UINT32hwiIndex; UINT32intSave; #if(LOSCFG_KERNEL_RUNSTOP==1) SCB->SCR&=(UINT32)~((UINT32)SCB_SCR_SLEEPDEEP_Msk); #endif intSave=LOS_IntLock(); ⑴g_intCount++; LOS_IntRestore(intSave); ⑵hwiIndex=HalIntNumGet(); OsHookCall(LOS_HOOK_TYPE_ISR_ENTER,hwiIndex); ⑶HalPreInterruptHandler(hwiIndex); #if(OS_HWI_WITH_ARG==1) if(g_hwiHandlerForm[hwiIndex].pfnHandler!=0){ g_hwiHandlerForm[hwiIndex].pfnHandler((VOID*)g_hwiHandlerForm[hwiIndex].pParm); } #else if(g_hwiHandlerForm[hwiIndex]!=0){ ⑷g_hwiHandlerForm[hwiIndex](); } #endif ⑸HalAftInterruptHandler(hwiIndex); OsHookCall(LOS_HOOK_TYPE_ISR_EXIT,hwiIndex); intSave=LOS_IntLock(); ⑹g_intCount--; LOS_IntRestore(intSave); } 3、开关中断 最后,分享下开、关中断的相关知识,开、关中断分别指的是: 开中断 执行完毕特定的短暂的程序,打开中断,可以响应中断。 关中断 为了保护执行的程序不被打断,关闭相应外部的中断。 对应的开、关中断的函数定义在文件 kernel\arch\include\los_context.h 中,代码如下。⑴处的 UINT32 LOS_IntLock(VOID)会关闭中断,暂停响应中断。⑵处的函数 VOID LOS_IntRestore(UINT32 intSave)可以用来恢复 UINT32LOS_IntLock(VOID)函数关闭的中断,UINT32 LOS_IntLock(VOID)的返回值作为 VOIDLOS_IntRestore(UINT32 intSave)的参数进行恢复中断。⑶处的函数 UINT32 LOS_IntUnLock(VOID)会使能中断,可以响应中断。 UINTPTRHalIntLock(VOID); ⑴#defineLOS_IntLockHalIntLock VOIDHalIntRestore(UINTPTRintSave); ⑵#defineLOS_IntRestoreHalIntRestore UINTPTRHalIntUnLock(VOID); ⑶#defineLOS_IntUnLockHalIntUnLock 可以看出,LOS_IntLock、LOS_IntRestore 和 LOS_IntUnLock 是定义的宏,他们对应定义在文件kernel\arch\arm\cortex-m7\gcc\los_dispatch.S 中的汇编函数,源码如下。我们分析下这些汇编函数。寄存器 PRIMASK 是单一 bit 位的寄存器,置为 1 后,就关掉所有可屏蔽异常,只剩下 NMI 和硬故障 HardFault 异常可以响应。默认值是 0,表示没有关闭中断。汇编指令 CPSID I 会设置 PRIMASK=1,关闭中断,指令 CPSIEI 设置 PRIMASK=0,开启中断。 ⑴处 HalIntLock 函数把寄存器 PRIMASK 数值写入寄存器 R0 返回,并执行 CPSIDI 关闭中断。⑵处 HalIntUnLock 函数把寄存器 PRIMASK 数值写入寄存器 R0 返回,并执行指令 CPSIEI 开启中断。两个函数的返回结果可以传递给⑶处 HalIntRestore 函数,把寄存器状态数值写入寄存器 PRIMASK,用于恢复之前的中断状态。不管是 HalIntLock 还是 HalIntUnLock,都可以和 ArchIntRestore 配对使用。 .typeHalIntLock,%function .globalHalIntLock HalIntLock: .fnstart .cantunwind ⑴MRSR0,PRIMASK CPSIDI BXLR .fnend .typeHalIntUnLock,%function .globalHalIntUnLock HalIntUnLock: .fnstart .cantunwind ⑵MRSR0,PRIMASK CPSIEI BXLR .fnend .typeHalIntRestore,%function .globalHalIntRestore HalIntRestore: .fnstart .cantunwind ⑶MSRPRIMASK,R0 BXLR .fnend 小结 本文带领大家一起剖析了鸿蒙轻内核的中断模块的源代码,掌握中断相关的概念,中断初始化操作,中断创建、删除,开关中断操作等。后续也会陆续推出更多的分享文章,敬请期待,也欢迎大家分享学习、使用鸿蒙轻内核的心得,有任何问题、建议,都可以留言给我们:gitee.com/openharmony… 。为了更容易找到鸿蒙轻内核代码仓,建议访问 gitee.com/openharmony… ,关注 Watch、点赞 Star、并 Fork 到自己账户下,谢谢。 点击关注,第一时间了解华为云新鲜技术~

优秀的个人博客,低调大师

Intelli J中好用和提高生产力的插件:Lombok 和Free Mybatis Plugin

开头: 做过Java的同学都知道,对编写Bean.class,要写很多Setter和Getter函数,当然我们可以利用IDE中的自带的Setter,Getter插件,完成Bean的属性函数编写 例如,在Mac 中的Intelli J中 直接用command+N,或者右键点击Generate 当然,这种方式还是不够简洁,当属性特别多的时候,整个类都是setter和getter函数,挺烦人的;当你要删除和添加属性的时候,也会很繁琐 Lombok插件: 在Intelli J,Lombok插件就应运而生了,我们就来直接看效果吧, import lombok.Data; @Data public class Product { private int id; } 直接使用类的声明上面添加@Data,或者在属性上添加@Setter或者@Getter import lombok.Getter; import lombok.Setter; public class Product { @Setter @Getter private int id; } 当然,利用@AllArgsConstructor,也可以自动生成构造函数 import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor //所有参数作为构造函数 public class Product { private int id; } 原理: 利用注解,在编译的时期,通过一种抽象语法树,自动匹配和生成Setter和Getter函数 下面就是编译后的.class文件,可以看出整个过程和结果还是很容易理解的,只要你理解注解的原理 // // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // public class Product { private int id; public Product() { } public int getId() { return this.id; } public void setId(int id) { this.id = id; } public boolean equals(Object o) { if (o == this) { return true; } else if (!(o instanceof Product)) { return false; } else { Product other = (Product)o; if (!other.canEqual(this)) { return false; } else { return this.getId() == other.getId(); } } } protected boolean canEqual(Object other) { return other instanceof Product; } public int hashCode() { int PRIME = true; int result = 1; int result = result * 59 + this.getId(); return result; } public String toString() { return "Product(id=" + this.getId() + ")"; } } 2.Free Mybaits Plugin: 做Java EE的时候,用到最多的ORM框架应该就是MyBaits了 MyBaits的主要结构就是service.java,mapper.java,mapper.xml组成,可以简单地理解下,service主要是提供业务接口的,mapper.class就是提供数据库接口的,mapper.xml就是操作数据库的;差不多就是这样的流程,service-->mapper.java-->mapper.xml,其中,mapper.java也和mapper.xml一一对应 如果我们手动编写这些代码,是不是很繁琐,本来Mybaits是帮我们屏蔽了很多数据库操作的细节的,可是,我们还是要编写很多代码和执行很多操作 当然肯定会有各种插件来帮我们减少代码量和操作量,针对Mybaits,国人做了一个插件:https://github.com/wuzhizhan/free-idea-mybatis,这个插件怎么用呢? 请打开这个链接,里面说得很详细, 安装方法: 以Lombok为例 1.直接在Intelli J中Preference--->Plugins--->搜索Lombok 2.上面的方法,可能会经常读取超时的问题,因为我们就需要先把插件文件下载本地,然后从本地安装下载 首先直接百度搜索插件,然后下载本地(推荐方法2) 然后,从Preference--->Plugins--->从本地安装 总结: 在Java这个庞大可持续的生态链中,各种个样的插件都很多,紧跟最新技术发展,那我们的编程效率就会高很多,我们就可以专心在业务中去。当然也要懂基本的原理,遇到问题才能从容应对

优秀的个人博客,低调大师

【Java入门提高篇】Day25 史上最详细的HashMap红黑树解析

当当当当当当当,好久不见,最近又是换工作,又是换房子,忙的不可开交,断更了一小段时间,最重要的一篇迟迟出不来,每次都犹抱琵琶半遮面,想要把它用通俗易懂的方式进行说明,确实有一定的难度,可愁煞我也,但自己挖的坑,哭着也要把它补上。请允许我当一回标题党。 好了,言归正传,本篇主要内容便是介绍HashMap的男二号——TreeNode(男一号还是给Node吧,毕竟是TreeNode的爷爷,而且普通节点一般来说也比TreeNode要多),本篇主要从以下几个方面介绍: 1.红黑树介绍 2.TreeNode结构 3.树化的过程 4.红黑树的左旋和右旋 5.TreeNode的左旋和右旋 6.红黑树的插入 7.TreeNode的插入 8.红黑树的删除 9.TreeNode的删除 讲解红黑树的部分算是理论部分,讲解TreeNode的部分则是代码实践部分,配合服用效果更加。 保守估计,仔细食用本篇大约需要半小时,请各位细细品尝。 红黑树介绍 什么是红黑树?嗯,首先,它是一颗树,所谓的树,便是长的像这样的东西 不像树?emmmm,你把它想象成一颗倒过来的树就好了,A~H都是树的节点,每个节点有零个或者多个子节点,或者说多个孩子,但除了根节点以外,每个节点都只有一个父节点,也称只有一个父亲(老王嘿嘿一笑)。最上面的A是根节点,最下面的D、H、F、G是叶子节点。每一个非根节点有且只有一个父节点;树是具有一层一层的层次结构,这里A位于第一层,B、C位于第二层,依次类推。将左边的B节点部分(包括BDEH)拿出来,则又是一颗树,称为树的子树。 好了,知道树是什么东西了,那么红黑树是什么样的呢? 红黑树,本质上来说是一颗二叉搜索树。嗯,还是先说说这个二叉搜索树吧。二叉代表它的节点最多有两个子节点,而且左右有顺序,不能颠倒,分别叫左孩子和右孩子,这两个节点互为兄弟节点,嗯,其实叫法根现实里的叫法差不多,以下图为例,4、9互为兄弟,7是他们的父亲,9是2的叔叔,8是2的堂兄弟,很简单吧。说完了称谓,再来说说用途,既然叫做搜索树表示它的用途是为了更快的搜索和查找而设计的,所以这棵树本身满足一定的排序规则,即树中的任何节点的值大于它的左孩子,且小于它的右孩子。任意节点的左、右子树也分别为二叉查找树。嗯,结合下图意会一下: 而红黑树,就跟它的名字一样,又红又黑,红黑并进,理实交融,节点是非红即黑的,看起来就像这样: 红黑树的主要特性: (1)每个节点要么是黑色,要么是红色。(节点非黑即红) (2)根节点是黑色。 (3)每个叶子节点(NIL)是黑色。 (4)如果一个节点是红色的,则它的子节点必须是黑色的。(也就是说父子节点不能同时为红色) (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。(这一点是平衡的关键) 说简单也简单,其实就是一颗比较平衡的又红又黑的二叉树嘛。 TreeNode结构 既然我们已经知道红黑树长什么样了,那么我们再来看看HashMap中的TreeNode代码里是如何表示的: /** * 用于Tree bins 的Entry。 扩展LinkedHashMap.Entry(进而扩展Node),因此可以用作常规节点或链接节点的扩展。 */ static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; // 红黑树父节点 TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // 删除后需要取消链接 boolean red; TreeNode(int hash, K key, V val, Node<K,V> next) { super(hash, key, val, next); } //省略后续代码 TreeNode继承自LinkedHashMap中的内部类——LinkedHashMap.Entry,而这个内部类又继承自Node,所以算是Node的孙子辈了。我们再来看看它的几个属性,parent用来指向它的父节点,left指向左孩子,right指向右孩子,prev则指向前一个节点(原链表中的前一个节点),注意,这些字段跟Entry,Node中的字段一样,是使用默认访问权限的,所以子类可以直接使用父类的属性。 树化的过程 在前几篇中已经有所介绍,当HashMap桶中的元素个数超过一定数量时,就会树化,也就是将链表转化为红黑树的结构。 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) { ...省略部分代码... else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); //当桶中元素个数超过阈值(8)时就进行树化 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); break; } ...省略部分代码... } final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { //将节点替换为TreeNode TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) //hd指向头结点 hd = p; else { //这里其实是将单链表转化成了双向链表,tl是p的前驱,每次循环更新指向双链表的最后一个元素,用来和p相连,p是当前节点 p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) //将链表进行树化 hd.treeify(tab); } } 从代码中可以看到,在treeifyBin函数中,先将所有节点替换为TreeNode,然后再将单链表转为双链表,方便之后的遍历和移动操作。而最终的操作,实际上是调用TreeNode的方法treeify进行的。 final void treeify(Node<K,V>[] tab) { //树的根节点 TreeNode<K,V> root = null; //x是当前节点,next是后继 for (TreeNode<K,V> x = this, next; x != null; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null; //如果根节点为null,把当前节点设置为根节点 if (root == null) { x.parent = null; x.red = false; root = x; } else { K k = x.key; int h = x.hash; Class<?> kc = null; //这里循环遍历,进行二叉搜索树的插入 for (TreeNode<K,V> p = root;;) { //p指向遍历中的当前节点,x为待插入节点,k是x的key,h是x的hash值,ph是p的hash值,dir用来指示x节点与p的比较,-1表示比p小,1表示比p大,不存在相等情况,因为HashMap中是不存在两个key完全一致的情况。 int dir, ph; K pk = p.key; if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; //如果hash值相等,那么判断k是否实现了comparable接口,如果实现了comparable接口就使用compareTo进行进行比较,如果仍旧相等或者没有实现comparable接口,则在tieBreakOrder中比较 else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x; //进行插入平衡处理 root = balanceInsertion(root, x); break; } } } } //确保给定节点是桶中的第一个元素 moveRootToFront(tab, root); } //这里不是为了整体排序,而是为了在插入中保持一致的顺序 static int tieBreakOrder(Object a, Object b) { int d; //用两者的类名进行比较,如果相同则使用对象默认的hashcode进行比较 if (a == null || b == null || (d = a.getClass().getName(). compareTo(b.getClass().getName())) == 0) d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1); return d; } 这里的逻辑其实不复杂,仅仅是循环遍历当前树,然后找到可以该节点可以插入的位置,依次和遍历节点比较,比它大则跟其右孩子比较,小则与其左孩子比较,依次遍历,直到找到左孩子或者右孩子为null的位置进行插入。 真正复杂一点的地方在于balanceInsertion函数,这个函数中,将红黑树进行插入平衡处理,保证插入节点后仍保持红黑树的性质。这个函数稍后在TreeNode的插入中进行介绍,这里先看看moveRootToFront,这个函数是将root节点移动到桶中的第一个元素,也就是链表的首节点,这样做是因为在判断桶中元素类型的时候会对链表进行遍历,将根节点移动到链表前端可以确保类型判断时不会出现错误。 /** * 把给定节点设为桶中的第一个元素 */ static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) { int n; if (root != null && tab != null && (n = tab.length) > 0) { int index = (n - 1) & root.hash; //first指向链表第一个节点 TreeNode<K,V> first = (TreeNode<K,V>)tab[index]; if (root != first) { //如果root不是第一个节点,则将root放到第一个首节点位置 Node<K,V> rn; tab[index] = root; TreeNode<K,V> rp = root.prev; if ((rn = root.next) != null) ((TreeNode<K,V>)rn).prev = rp; if (rp != null) rp.next = rn; if (first != null) first.prev = root; root.next = first; root.prev = null; } //这里是防御性编程,校验更改后的结构是否满足红黑树和双链表的特性 //因为HashMap并没有做并发安全处理,可能在并发场景中意外破坏了结构 assert checkInvariants(root); } } 红黑树的左旋和右旋 左旋和右旋,顾名思义嘛,就是将节点以某个节点为中心向左或者向右进行旋转操作以保持二叉树的平衡,让我们看图说话: 图画的有点大。将就着看一下吧,左旋相当于以要旋转的节点为中心,将子树整体向左旋转,该节点变成子树的根节点,原来的父节点A变成了左孩子,如果右孩子C有左孩子D,则将其变为A的右孩子。说起来好像有点绕,可以联系图进行形象化的理解,当节点A向左旋转之后,C的左孩子D可以理解为因为重力作用掉到A的右孩子位置,嗯,就是这样。右旋也是类似理解即可。 TreeNode的左旋和右旋 了解了左旋和右旋,让我们看看代码里是怎样实现的: /** * 左旋 */ static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) { //这里的p即上图的A节点,r指向右孩子即C,rl指向右孩子的左孩子即D,pp为p的父节点 TreeNode<K,V> r, pp, rl; if (p != null && (r = p.right) != null) { if ((rl = p.right = r.left) != null) rl.parent = p; //将p的父节点的孩子节点指向r if ((pp = r.parent = p.parent) == null) (root = r).red = false; else if (pp.left == p) pp.left = r; else pp.right = r; //将p置为r的左节点 r.left = p; p.parent = r; } return root; } /** * 右旋 */ static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) { //这里的p即上图的A节点,l指向左孩子即C,lr指向左孩子的右孩子即E,pp为p的父节点 TreeNode<K,V> l, pp, lr; if (p != null && (l = p.left) != null) { if ((lr = p.left = l.right) != null) lr.parent = p; if ((pp = l.parent = p.parent) == null) (root = l).red = false; else if (pp.right == p) pp.right = l; else pp.left = l; l.right = p; p.parent = l; } return root; } 其实,也很简单嘛。23333 红黑树的插入 现在来看看一个比较麻烦一点的操作,红黑树的插入,首先找到这个节点要插入的位置,即一层一层比较,大的放右边,小的放左边,直到找到为null的节点放入即可,但是如何在插入的过程保持红黑树的特性呢,想想好像比较头疼,但是再仔细想想其实就会发现,其实只有这么几种情况: 1.插入的为根节点,则直接把颜色改成黑色即可。 2.插入的节点的父节点是黑色节点,则不需要调整,因为插入的节点会初始化为红色节点,红色节点是不会影响树的平衡的。 3.插入的节点的祖父节点为null,即插入的节点的父节点是根节点,直接插入即可(因为根节点肯定是黑色)。 4.插入的节点父节点和祖父节点都存在,并且其父节点是祖父节点的左节点。这种情况稍微麻烦一点,又分两种子情况: i.插入节点的叔叔节点是红色,则将父亲节点和叔叔节点都改成黑色,然后祖父节点改成红色即可。 ii.插入节点的叔叔节点是黑色或不存在: a.若插入节点是其父节点的右孩子,则将其父节点左旋, b.若为左孩子,则将其父节点变成黑色节点,将其祖父节点变成红色节点,然后将其祖父节点右旋。 5.插入的节点父节点和祖父节点都存在,并且其父节点是祖父节点的右节点。这种情况跟上面是类似的,分两种子情况: i.插入节点的叔叔节点是红色,则将父亲节点和叔叔节点都改成黑色,然后祖父节点改成红色即可。 ii.插入节点的叔叔节点是黑色或不存在: a.若插入节点是其父节点的左孩子,则将其父节点右旋 b.若为右孩子,则将其父节点变成黑色节点,将其祖父节点变成红色节点,然后将其祖父节点左旋。 然后重复进行上述操作,直到变成1或2情况时则结束变换。说半天,可能还是云里雾里,一图胜千言,让我们从无到有构建一颗红黑树,假设插入的顺序为:10,5,9,3,6,7,19,32,24,17(数字是我拍脑袋瞎想的。) 先来插个10,为情景1,直接改成黑色即可,再插入5,为情景2,比10小,放到10的左孩子位置,插入9,比10小,但是比5大,放到5的右孩子位置,此时,为情景4iia,左旋后变成了情景4iib,变色右旋即可完成转化。插入3后为情景4i,将父节点和叔叔节点同时变色即可,插入6不需要调整,插入7后为情景5i,变色即可。插入19不需要调整,插入32,变成了5iib,左旋变色即可,插入24,变成5iia,右旋后变成5i,变色即可,最后插入17,完美。 看图说话是不是就简单明了了,看在我画图这么辛苦的份上,点个关注给个赞可好(滑稽)。 TreeNode的插入 了解了红黑树的删除之后,我们再来看下TreeNode中是怎样用代码实现的: static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) { x.red = true; for (TreeNode<K,V> xp, xpp, xppl, xppr;;) { //情景1:父节点为null if ((xp = x.parent) == null) { x.red = false; return x; } //情景2,3:父节点是黑色节点或者祖父节点为null else if (!xp.red || (xpp = xp.parent) == null) return root; //情景4:插入的节点父节点和祖父节点都存在,并且其父节点是祖父节点的左节点 if (xp == (xppl = xpp.left)) { //情景4i:插入节点的叔叔节点是红色 if ((xppr = xpp.right) != null && xppr.red) { xppr.red = false; xp.red = false; xpp.red = true; x = xpp; } //情景4ii:插入节点的叔叔节点是黑色或不存在 else { //情景4iia:插入节点是其父节点的右孩子 if (x == xp.right) { root = rotateLeft(root, x = xp); xpp = (xp = x.parent) == null ? null : xp.parent; } //情景4iib:插入节点是其父节点的左孩子 if (xp != null) { xp.red = false; if (xpp != null) { xpp.red = true; root = rotateRight(root, xpp); } } } } //情景5:插入的节点父节点和祖父节点都存在,并且其父节点是祖父节点的右节点 else { //情景5i:插入节点的叔叔节点是红色 if (xppl != null && xppl.red) { xppl.red = false; xp.red = false; xpp.red = true; x = xpp; } //情景5ii:插入节点的叔叔节点是黑色或不存在 else {· //情景5iia:插入节点是其父节点的左孩子 if (x == xp.left) { root = rotateRight(root, x = xp); xpp = (xp = x.parent) == null ? null : xp.parent; } //情景5iib:插入节点是其父节点的右孩子 if (xp != null) { xp.red = false; if (xpp != null) { xpp.red = true; root = rotateLeft(root, xpp); } } } } } } 其实就是一毛一样的,对号入座即可。 红黑树的删除 讲完插入,接下来我们来说说删除,删除的话,比插入还要复杂一点,请各位看官先深呼吸,做好阅读准备。 之前已经说过,红黑树是一颗特殊的二叉搜索树,所以进行删除操作时,其实是先进行二叉搜索树的删除,然后再进行调整。所以,其实这里分为两部分内容:1.二叉搜索树的删除,2.红黑树的删除调整。 二叉搜索树的删除主要有这么几种情景: 情景1:待删除的节点无左右孩子。 情景2:待删除的节点只有左孩子或者右孩子。 情景3:待删除的节点既有左孩子又有右孩子。 对于情景1,直接删除即可,情景2,则直接把该节点的父节点指向它的左孩子或者右孩子即可,情景3稍微复杂一点,需要先找到其右子树的最左孩子(或者左子树的最右孩子),即左(右)子树中序遍历时的第一个节点,然后将其与待删除的节点互换,最后再删除该节点(如果有右子树,则右子树上位)。总之,就是先找到它的替代者,找到之后替换这个要删除的节点,然后再把这个节点真正删除掉。 其实二叉搜索树的删除总体来说还是比较简单的,删除完之后,如果替代者是红色节点,则不需要调整,如果是黑色节点,则会导致左子树和右子树路径中黑色节点数量不一致,需要进行红黑树的调整,跟上面一样,替代节点为其父节点的左孩子与右孩子的情况类似,所以这里只说其为左孩子的情景(PS:上一步的寻找替换节点使用的是右子树的最左节点,所以该节点如果有孩子,只能是右孩子): 情景1:只有右孩子且为红色,直接用右孩子替换该节点然后变成黑色即可。 (D代表替代节点,即要被删除的节点,之前在经过二叉搜索树的删除后,D节点其实已经被删除了,这里为了方便理解这个变化过程,所以把这个节点也画出来了,所以当前的初始状态是待删除节点与其替换节点互换位置与颜色之后的状态) 情景2:只有右孩子且为黑色,那么删除该节点会导致父节点的左子树路径上黑色节点减一,此时只能去借助右子树,从右子树中借一个红色节点过来即可,具体取决于右子树的情况,这里又分成两种: i.兄弟节点是红色,则此时父节点是黑色,且兄弟节点肯定有两个孩子,且兄弟节点的左右子树路径上均有两个黑色节点,此时只需将兄弟节点与父节点颜色互换,然后将父节点左旋,左旋后,兄弟节点的左子树SL挂到了父节点p的右孩子位置,这时会导致p的右子树路径上的黑色节点比左子树多一,此时再SL置为红色即可。 ii.兄弟节点是黑色,那么就只能打它孩子的主意了,这里主要关注远侄子(兄弟节点的右孩子,即SR)的颜色情况,这里分成两种情况: a.远侄子SR是黑色,近侄子任意(白色代表颜色可为任意颜色),则先将S转为红色,然后右旋,再将SL换成P节点颜色,P涂成黑色,S也涂成黑色,再进行左旋即可。其实简单说就是SL上位,替换父节点位置。 b.远侄子SR为红色,近侄子任意(该子树路径中有且仅有一个黑色节点),则先将兄弟节点与父节点颜色互换,将SR涂成黑色,再将父节点左旋即可。 emmmm...好像也不是很麻烦嘛(逃)。 TreeNode的删除节点 TreeNode删除节点其实也是两步走,先进行二叉搜索树的删除,然后再进行红黑树的调整,跟之前的情况分析是一致的。 final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) { ...... //p是待删除节点,replacement用于后续的红黑树调整,指向的是p或者p的继承者。 //如果p是叶子节点,p==replacement,否则replacement为p的右子树中最左节点 if (replacement != p) { //若p不是叶子节点,则让replacement的父节点指向p的父节点 TreeNode<K,V> pp = replacement.parent = p.parent; if (pp == null) root = replacement; else if (p == pp.left) pp.left = replacement; else pp.right = replacement; p.left = p.right = p.parent = null; } //若待删除的节点p时红色的,则树平衡未被破坏,无需进行调整。 //否则删除节点后需要进行调整 TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement); //p为叶子节点,则直接将p从树中清除 if (replacement == p) { // detach TreeNode<K,V> pp = p.parent; p.parent = null; if (pp != null) { if (p == pp.left) pp.left = null; else if (p == pp.right) pp.right = null; } } } 麻烦的地方就在删除节点后的调整了,所有逻辑都在balanceDeletion函数里,两个参数分别表示根节点和删除节点的继承者,来看看它的具体实现: static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) { for (TreeNode<K,V> xp, xpl, xpr;;) { //x为空或x为根节点,直接返回 if (x == null || x == root) return root; //x为根节点,染成黑色,直接返回(因为调整过后,root并不一定指向删除操作过后的根节点,如果之前删除的是root节点,则x将成为新的根节点) else if ((xp = x.parent) == null) { x.red = false; return x; } //如果x为红色,则无需调整,返回 else if (x.red) { x.red = false; return root; } //x为其父节点的左孩子 else if ((xpl = xp.left) == x) { //如果它有红色的兄弟节点xpr,那么它的父亲节点xp一定是黑色节点 if ((xpr = xp.right) != null && xpr.red) { xpr.red = false; xp.red = true; //对父节点xp做左旋转 root = rotateLeft(root, xp); //重新将xp指向x的父节点,xpr指向xp新的右孩子 xpr = (xp = x.parent) == null ? null : xp.right; } //如果xpr为空,则向上继续调整,将x的父节点xp作为新的x继续循环 if (xpr == null) x = xp; else { //sl和sr分别为其近侄子和远侄子 TreeNode<K,V> sl = xpr.left, sr = xpr.right; if ((sr == null || !sr.red) && (sl == null || !sl.red)) { xpr.red = true; //若sl和sr都为黑色或者不存在,即xpr没有红色孩子,则将xpr染红 x = xp; //本轮结束,继续向上循环 } else { //否则的话,就需要进一步调整 if (sr == null || !sr.red) { if (sl != null) //若左孩子为红,右孩子不存在或为黑 sl.red = false; //左孩子染黑 xpr.red = true; //将xpr染红 root = rotateRight(root, xpr); //右旋 xpr = (xp = x.parent) == null ? null : xp.right; //右旋后,xpr指向xp的新右孩子,即上一步中的sl } if (xpr != null) { xpr.red = (xp == null) ? false : xp.red; //xpr染成跟父节点一致的颜色,为后面父节点xp的左旋做准备 if ((sr = xpr.right) != null) sr.red = false; //xpr新的右孩子染黑,防止出现两个红色相连 } if (xp != null) { xp.red = false; //将xp染黑,并对其左旋,这样就能保证被删除的X所在的路径又多了一个黑色节点,从而达到恢复平衡的目的 root = rotateLeft(root, xp); } //到此调整已经完毕,进入下一次循环后将直接退出 x = root; } } } //x为其父节点的右孩子,跟上面类似 else { // symmetric if (xpl != null && xpl.red) { xpl.red = false; xp.red = true; root = rotateRight(root, xp); xpl = (xp = x.parent) == null ? null : xp.left; } if (xpl == null) x = xp; else { TreeNode<K,V> sl = xpl.left, sr = xpl.right; if ((sl == null || !sl.red) && (sr == null || !sr.red)) { xpl.red = true; x = xp; } else { if (sl == null || !sl.red) { if (sr != null) sr.red = false; xpl.red = true; root = rotateLeft(root, xpl); xpl = (xp = x.parent) == null ? null : xp.left; } if (xpl != null) { xpl.red = (xp == null) ? false : xp.red; if ((sl = xpl.left) != null) sl.red = false; } if (xp != null) { xp.red = false; root = rotateRight(root, xp); } x = root; } } } } } 呼。。。终于。。酝酿了好多天的一篇文章总算是写完了,为了尽量确认转换的准确性,找了很多资料进行参考,过程中花了不少时间,曾多次准备放弃。。。不过总算是没有死在娘胎里,也算是完成了一桩心事,开心。. 之后还会继续更新,欢迎大家继续关注。也欢迎大家前来打脸真正重要的东西,用眼睛是看不见的。

优秀的个人博客,低调大师

“docker-app”实用工具分享,大大提高 Compose 文件复用率

本文首发自“Docker公司”公众号(ID:docker-cn)编译丨小东每周一、三、五 与您不见不散! Docker Compose 在开发人员中非常流行,它用来描述应用程序。目前,GitHub 上有超过30万个 Docker Compose 文件。通过在 docker-compose.yml 文件中对一组服务进行描述,就可以在 Docker 上用一条命令轻松的启动一个复杂的多服务应用程序(或简单的单服务应用程序)。这种易用性使得 Docker Compose 非常适合开发团队快速开展项目。 随着时间的推移,Compose 不断发展并添加了许多功能可以在将相同的应用程序部署到生产环境时提供帮助,例如,指定大量副本、内存资源限制或自定义系统日志服务器。但这些属性可能会跟您自己的环境有所差异。有许多不同的策略来解决这个问题,但是最常见的就是依赖于复制和粘贴。例如,为在不同环境中运行的同一应用程序维护多个 Compose 文件是相当常见的,但这会导致了两个问题: 我们一直都在分享 Docker 镜像,但却没有一个很好的方法来共享使用它们的多服务应用程序; 开发人员和运维人员很难围绕 Compose 文件进行协作。这淡化了在代码中描述应用程序的其中一项关键优势 —— 即开发人员和操作人员使用同一个 Compose 文件的机会,并在产品投入生产之前捕获配置问题; 介绍 docker-app 解决这个问题的一种方法是通过构建一个与 Compose 相辅相成的工具来使它更容易用于共享和协作。请注意,这是实验性的,还有很多工作正在进行中,但我们希望获得早期采用者的反馈和意见,这会: 使基于Compose 的应用程序可以共享在 Docker Hub 和 DTR 上; 支持在应用程序描述和每个环境设置之间更紧密的关注点分离; 该实现包含一些额外的元数据文件和一个小型命令行工具。让我们来举个简单的例子。 使用下面的 Compose 文件。它启动一个HTTP服务器,当触发配置的端口时,它会显示出指定的文本。 version: '3.6' services: hello: image: hashicorp/http-echo command: ["-text", "hello world"] ports: - 5678:5678 用 docker-app 命令安装,让我们基于这个 Compose 文件创建一个应用程序包: $ docker-app init --single-file hello $ ls docker-compose.yml hello.dockerapp 这个应用程序包只是一个文本文件(或者是一个目录),在这个例子中叫做 hello.dockerapp。它包含三个YAML文档: 一些元数据 Compose 文件 应用程序的一些设置 它应该是这样的: # This section contains your application metadata. version: 0.1.0 name: hello description: "" maintainers: - name: yourusername email: "" targets: swarm: true kubernetes: true -- # This section contains the Compose file that describes your application services. version: '3.6' services: hello: image: hashicorp/http-echo command: ["-text", "hello world"] ports: - 5678:5678 -- # This section contains the default values for your application settings. {} 让我们编辑设置部分(替换 {})并为我们的应用程序添加以下默认值: port: 5678 text: hello development version: latest 然后修改 Compose 文件部分,添加一些变量: version: '3.6' services: hello: image: hashicorp/http-echo:${version} command: ["-text", "${text}"] ports: - ${port}:5678 最后,您可以通过使用所提供的默认值渲染 Compose 文件来进行测试。 $ docker-app render version: "3.6" services: hello: command: - -text - hello development image: hashicorp/http-echo:latest ports: - mode: ingress target: 5678 published: 5678 protocol: tcp 请注意,这些变量已经被设置值替换。之后,您可以像使用其他 Compose 文件一样来使用该 Compose 文件了。您可以将其保存到磁盘或 Docker 应用栈中亦或是使用 docker-compose 命令来启动应用程序。 $ docker-app render | docker-compose -f – up 这就是它有趣的地方。我们可以在运行时使用 --set 选项来覆盖这些设置。让我们指定不同的选项并再次运行渲染: $ docker-app render --set version=0.2.3 --set port=4567 --set text="hello production" version: "3.6" services: hello: command: - -text - hello production image: hashicorp/http-echo:0.2.3 ports: - mode: ingress target: 5678 published: 4567 protocol: tcp 请注意在生成的 Compose 文件中对端口和版本进行更改。 如果你愿意,你可以创建一个独立的配置文件来存储这些设置。 让我们用以下内容创建prod.yml: version: 0.2.3 text: hello production port: 4567 然后,您可以使用该配置文件显示 Compose 文件,如下所示: $ docker-app render -f prod.yml 这样就可以很容易地为不同的环境单独的设置文件了,从而减少了复制整个 Compose 文件的需要。 如果您想要超越hello world,我们还准备了一些更高级的例子。 您可以在 Docker Compose 中使用环境变量支持来实现与上述类似的内容,但需要您自己编写工具来提供一个不错的用户界面。有了上述惯例,我们可以在上面创建更有趣的东西。 例如,我们可以构建相当有趣的自省工具,就像下面所示的那样,我们计划将简单的变量替换转换为更复杂的模板。 检查和部署应用程序包 docker-app 命令不仅提供了用不同设置来渲染 Compose 文件的方法。它还提供了一些实用工具来与它们进行交互。例如,如果有人给你一个 .dockerapp,这时你可以很容易地了解它的信息,特别是在运行时发现哪些设置是可用的,而不需要读取任何包代码。 $ docker-app inspect hello 0.1.0 Maintained by: gareth A hello world example of a Docker application package. Setting Default ------- ------- port 8080 text hello world version latest 一旦准备好部署应用程序的一个版本,您就可以使用子命令进行部署了。它的工作方式与 docker 应用栈的部署命令完全相同,因此您应该很熟悉这一点。例如,如果您使用的是Docker Desktop 或 Docker EE,那么您就可以将应用程序部署到 Kubernetes,同时覆盖一些暴露的设置。 $ docker-app deploy --set port=4567 --orchestrator=kubernetes docker-app 还有很多实用的工具,你可以在内置的帮助信息中找到,或者等待后续的文章推送。 感兴趣吗? 如果您感兴趣的话,可以浏览 https://github.com/docker/app 来访问 GitHub 仓库。您将会看到基本的文档和几个示例,以及下载最新版本(针对Windows、macOS或Linux)和应用程序源代码的说明。如果您在有任何问题、想法都可以在这个镜像仓库中提交给我们。

优秀的个人博客,低调大师

【Java入门提高篇】Day24 Java容器类详解(七)HashMap源码分析(下)

前两篇对HashMap这家伙的主要方法,主要算法做了一个详细的介绍,本篇主要介绍HashMap中默默无闻地工作着的集合们,包括KeySet,values,EntrySet,以及对应的迭代器:HashIterator,KeyIterator,ValueIterator,EntryIterator和fast-fail 机制。会介绍三个集合的作用以及它们中隐藏的惊人秘密。 KeySet 我们先来看看KeySet,HashMap中的成员变量keySet保存了所有的Key集合,事实上,这是继承自它的父类AbstractMap的成员变量: transient Set<K> keySet; 而keySet方法,也是覆盖了父类的方法: //AbstractMap 中的keySet方法 public Set<K> keySet() { Set<K> ks = keySet; if (ks == null) { ks = new AbstractSet<K>() { public Iterator<K> iterator() { return new Iterator<K>() { private Iterator<Entry<K,V>> i = entrySet().iterator(); public boolean hasNext() { return i.hasNext(); } public K next() { return i.next().getKey(); } public void remove() { i.remove(); } }; } public int size() { return AbstractMap.this.size(); } public boolean isEmpty() { return AbstractMap.this.isEmpty(); } public void clear() { AbstractMap.this.clear(); } public boolean contains(Object k) { return AbstractMap.this.containsKey(k); } }; keySet = ks; } return ks; } //HashMap 中的keySet方法 /** * 返回一个键值的集合视图,该集合由map支持,因此对map的更改会反映在集合中,反之亦然。 * 如果在对集合进行迭代的过程中修改了map中的映射(除了通过迭代器的删除操作),迭代的结果是未定义的。 * 该集合支持元素删除,通过Iterator.remove,Set.remove,removeAll,retainAll和clear操作 * 从映射中删除相应的映射。 它不支持add或addAll操作。 */ public Set<K> keySet() { Set<K> ks = keySet; if (ks == null) { ks = new KeySet(); keySet = ks; } return ks; } 可以看到,AbstractMap中keySet是一个AbstractSet类型,而覆盖后的keySet方法中,keySet被赋值为KeySet类型。翻翻构造器可以发现,在构造器中并没有初始化keySet,而是在KeySet方法中对keySet进行的初始化(HashMap中都是使用类似的懒加载机制),KeySet是HashMap中的一个内部类,让我们再来看看这个KeySet类型的全貌: final class KeySet extends AbstractSet<K> { public final int size() { return size; } public final void clear() { this.clear(); } public final Iterator<K> iterator() { return new KeyIterator(); } public final boolean contains(Object o) { return containsKey(o); } public final boolean remove(Object key) { return removeNode(hash(key), key, null, false, true) != null; } public final Spliterator<K> spliterator() { return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0); } public final void forEach(Consumer<? super K> action) { Node<K,V>[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) action.accept(e.key); } if (modCount != mc) throw new ConcurrentModificationException(); } } } 其实KeySet就是继承自AbstractSet,并覆盖了其中的大部分方法,遍历KeySet时,会使用其中的KeyIterator,至于Spliterator,是为并行遍历设计的,一般是用于Stream的并行操作。forEach方法则是用于遍历操作,将函数式接口操作action应用于每一个元素,我们来看一个小栗子: public class Test { public static void main(String[] args) { Map<String, Integer> map = new HashMap(); map.put("小明", 66); map.put("小李", 77); map.put("小红", 88); map.put("小刚", 89); map.put("小力", 90); map.put("小王", 91); map.put("小黄", 92); map.put("小青", 93); map.put("小绿", 94); map.put("小黑", 95); map.put("小蓝", 96); map.put("小紫", 97); map.put("小橙", 98); map.put("小赤", 99); map.put("Frank", 100); Set<String> ks = map.keySet(); System.out.printf("keySet:%s,keySet的大小:%d,keySet中是否包含Frank:%s", ks, ks.size(), ks.contains("Frank")); System.out.println(); ks.forEach((item) -> System.out.println(item)); } } 输出如下: keySet:[小刚, 小橙, 小蓝, 小力, 小青, 小黑, 小明, 小李, 小王, 小紫, 小红, 小绿, Frank, 小黄, 小赤],keySet的大小:15,keySet中是否包含Frank:true 小刚 小橙 小蓝 小力 小青 小黑 小明 小李 小王 小紫 小红 小绿 Frank 小黄 小赤 如果不记得这个AbstractMap和AbstractSet在容器框架中是什么地位,可以往前翻翻这系列文章的第一篇,看看容器家族的族谱。 但是说了这么多,这个keySet。里面的元素是什么时候放进去的呢?我们自然会想到,大概就是调用put方法往里添加元素的时候,顺便把key放进keySet中,完美!让我们再回顾一下putVal方法,来看看是不是这样的: final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //如果当前table未初始化,则先重新调整大小至初始容量 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //(n-1)& hash 这个地方即根据hash求序号,想了解更多散列相关内容可以查看下一篇 if ((p = tab[i = (n - 1) & hash]) == null) //不存在,则新建节点 tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; //先找到对应的node if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) //如果是树节点,则调用相应的putVal方法,这部分放在第三篇内容里 //todo putTreeVal 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 //如果链表长度达到树化的最大长度,则进行树化,该函数内容也放在第三篇 //todo treeifyBin treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //如果已存在该key的映射,则将值进行替换 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; } emmmmm,好像没找到?你也许会想,会不会是在TreeNode的putTreeVal方法或者在treeifyBin方法中对key进行插入?好了好了,不要再翻了,其实这个奥秘隐藏在KeySet的迭代器中,再回头看看,它的迭代器返回的是一个KeyIterator,而KeyIterator也是HashMap中的一个内部类,继承自HashMap中的另一个内部类HashIterator。 HashIterator 让我们带着这个疑问,来看看这个HashIterator类里到底有什么玄机: abstract class HashIterator { //指向下一个节点 Node<K,V> next; //当前节点 Node<K,V> current; //为实现 fast-fail 机制而设置的期望修改数 int expectedModCount; //当前遍历到的序号 int index; HashIterator() { expectedModCount = modCount; Node<K,V>[] t = table; current = next = null; index = 0; if (t != null && size > 0) { // 移动到第一个非null节点 do {} while (index < t.length && (next = t[index++]) == null); } } public final boolean hasNext() { return next != null; } final Node<K,V> nextNode() { Node<K,V>[] t; Node<K,V> e = next; // fast-fail 机制的实现 即在迭代器往后遍历时,每次都检测expectedModCount是否和modCount相等 // 不相等则抛出ConcurrentModificationException异常 if (modCount != expectedModCount) throw new ConcurrentModificationException(); //如果遍历越界,则抛出NoSuchElementException异常 if (e == null) throw new NoSuchElementException(); if ((next = (current = e).next) == null && (t = table) != null) { //如果遍历到末尾,则跳到table中下一个不为null的节点处 do {} while (index < t.length && (next = t[index++]) == null); } return e; } public final void remove() { Node<K,V> p = current; if (p == null) throw new IllegalStateException(); if (modCount != expectedModCount) throw new ConcurrentModificationException(); current = null; K key = p.key; //移除节点 removeNode(hash(key), key, null, false, false); expectedModCount = modCount; } } 可以发现,在迭代器中,使用nextNode进行遍历时,先把next引用赋值给current,然后把next.next赋值给next,再获取了外部类HashMap中的table引用(t = table),这样就直接通过遍历table的方式来实现对key,value和entry的读取。 if ((next = (current = e).next) == null && (t = table) != null) { //如果遍历到末尾,则跳到table中下一个不为null的节点处 do {} while (index < t.length && (next = t[index++]) == null); } KeyIterator,ValueIterator,EntryIterator都是HashIterator的子类,实现也很简单,仅仅修改了泛型类型: final class KeyIterator extends HashIterator implements Iterator<K> { public final K next() { return nextNode().key; } } final class ValueIterator extends HashIterator implements Iterator<V> { public final V next() { return nextNode().value; } } final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> { public final Map.Entry<K,V> next() { return nextNode(); } } 这样keySet在遍历的时候,就可以通过它的迭代器去遍历访问外部类HashMap中的table,类似的,values和entrySet也是使用相似的方式进行遍历。 public Collection<V> values() { Collection<V> vs = values; if (vs == null) { vs = new Values(); values = vs; } return vs; } final class Values extends AbstractCollection<V> { public final int size() { return size; } public final void clear() { this.clear(); } public final Iterator<V> iterator() { return new ValueIterator(); } public final boolean contains(Object o) { return containsValue(o); } public final Spliterator<V> spliterator() { return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0); } public final void forEach(Consumer<? super V> action) { Node<K,V>[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) action.accept(e.value); } if (modCount != mc) throw new ConcurrentModificationException(); } } } public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; } final class EntrySet extends AbstractSet<Map.Entry<K,V>> { public final int size() { return size; } public final void clear() { this.clear(); } public final Iterator<Map.Entry<K,V>> iterator() { return new EntryIterator(); } public final boolean contains(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?,?> e = (Map.Entry<?,?>) o; Object key = e.getKey(); Node<K,V> candidate = getNode(hash(key), key); return candidate != null && candidate.equals(e); } public final boolean remove(Object o) { if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>) o; Object key = e.getKey(); Object value = e.getValue(); return removeNode(hash(key), key, value, true, true) != null; } return false; } public final Spliterator<Map.Entry<K,V>> spliterator() { return new EntrySpliterator<K,V>(HashMap.this, 0, -1, 0, 0); } public final void forEach(Consumer<? super Map.Entry<K,V>> action) { Node<K,V>[] tab; if (action == null) throw new NullPointerException(); if (size > 0 && (tab = table) != null) { int mc = modCount; for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) action.accept(e); } if (modCount != mc) throw new ConcurrentModificationException(); } } } 至此,这个未解之谜算是告一段落了。 transient 但是,细心的同学可能会发现,HashMap中的table,entrySet,keySet,value等成员变量,都是用transient修饰的,为什么要这样做呢? 首先,我们还是先说说这个transient是干嘛用的,这就要涉及Java中的序列化了,序列化是什么东西呢? Java中对象的序列化指的是将对象转换成以字节序列的形式来表示,这些字节序列包含了对象的数据和信息。一个序列化后的对象可以被写到数据库或文件中,也可用于网络传输,一般当我们使用缓存cache(内存空间不够有可能会本地存储到硬盘)或远程调用rpc(网络传输)的时候,经常需要让我们的实体类实现Serializable接口,目的就是为了让其可序列化。 当然,就像数据存储是为了读取那样,序列化后的最终目的是为了恢复成原先的Java对象,要不然序列化后干嘛呢,这个过程就叫做反序列化。 当我们使用实现Serializable接口的方式来进行序列化时,所有字段都会被序列化,那如果不想让某个字段被序列化(比如出于安全考虑,不将敏感字段序列化传输),便可以使用transient关键字来标志,表示不想让这个字段被序列化。 那么问题来了,存储节点信息的table用transient修饰了,那么序列化和反序列化的时候,数据还怎么传输??? emmmm,这又涉及到一个蛋疼的操作,序列化并没有那么简单,实现了Serializable接口后,在序列化时,会先检测这个类是否存在writeObject和readObject方法,如果存在,则调用相应的方法: /** * 将HashMap的实例状态保存到一个流中 */ private void writeObject(java.io.ObjectOutputStream s) throws IOException { int buckets = capacity(); // 写出threshold,loadfactor和所有隐藏的成员 s.defaultWriteObject(); s.writeInt(buckets); s.writeInt(size); internalWriteEntries(s); } /** * 从流中重构HashMap实例 */ private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { // 读取threshold,loadfactor和所有隐藏的成员 s.defaultReadObject(); reinitialize(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException("Illegal load factor: " + loadFactor); // 读取并忽略桶的数量 s.readInt(); // 读取映射的数量 int mappings = s.readInt(); if (mappings < 0) throw new InvalidObjectException("Illegal mappings count: " + mappings); else if (mappings > 0) { // (如果是0,则使用默认值) // Size the table using given load factor only if within // range of 0.25...4.0 float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f); float fc = (float)mappings / lf + 1.0f; int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ? DEFAULT_INITIAL_CAPACITY : (fc >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)fc)); float ft = (float)cap * lf; threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ? (int)ft : Integer.MAX_VALUE); SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap); @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] tab = (Node<K,V>[])new Node[cap]; table = tab; // 读取键值对信息,然后把映射插入HashMap实例中 for (int i = 0; i < mappings; i++) { @SuppressWarnings("unchecked") K key = (K) s.readObject(); @SuppressWarnings("unchecked") V value = (V) s.readObject(); putVal(hash(key), key, value, false, false); } } } 这确实是一个极其糟糕的设计。。。而且这里还是一个private方法。 那么直接使用默认的序列化不好吗?非要大费周章的骚操作一波?一部分原因是为了解决效率问题,因为HashMap中很多桶是空的,将其序列化没有任何意义,所以需要手动使用 writeObject() 方法,只序列化实际存储元素的数组。另一个很重要的原因便是,HashMap的存储是依赖于对象的hashCode的,而Object.hashCode()方法是依赖于具体虚拟机的,所以同一个对象,在不同虚拟机中的HashCode可能不同,那这样映射到的HashMap中的位置也不一样,这样序列化和反序列化的对象就不一样了。引用大神的一段话: For example, consider the case of a hash table. The physical representation is a sequence of hash buckets containing key-value entries. The bucket that an entry resides in is a function of the hash code of its key, which is not, in general, guaranteed to be the same from JVM implementation to JVM implementation. In fact, it isn't even guaranteed to be the same from run to run. Therefore, accepting the default serialized form for a hash table would constitute a serious bug. Serializing and deserializing the hash table could yield an object whose invariants were seriously corrupt. 蹩脚翻译一下: 例如,考虑散列表的情况。 它的物理存储是一系列包含键值条目的散列桶。 条目驻留的存储区是其密钥的哈希码的函数,通常,JVM的实现不保证相同。 事实上,它甚至不能保证每次运行都是一样的。 因此,接受哈希表的默认序列化形式将构成严重的错误。 对哈希表进行序列化和反序列化可能会产生不变性被严重损毁的对象。 好了,到此为止,这部分内容算是over了,后面会继续介绍HashMap中最麻烦的一部分,TreeNode让我们师母已呆 记得动动小手点个赞或者点个关注哦,如果觉得不错的话,也欢迎分享给你的朋友,让bug传播的更远一些,呸,说错了,让知识传播的更远一些如果写的有误的地方,欢迎大家及时指出,我会第一时间予以修正,也欢迎提出改进建议,之后还会继续更新,欢迎继续关注! 真正重要的东西,用眼睛是看不见的。

优秀的个人博客,低调大师

【Java入门提高篇】Day23 Java容器类详解(六)HashMap源码分析(中)

上一篇中对HashMap中的基本内容做了详细的介绍,解析了其中的get和put方法,想必大家对于HashMap也有了更好的认识,本篇将从了算法的角度,来分析HashMap中的那些函数。 HashCode 先来说说HashMap中HashCode的算法,在上一篇里,我们看到了HashMap中的put方法是这样的: public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } 那这个hash函数又是什么呢?让我们来看看它的真面目: /** * 将高位与低位进行与运算来计算哈希值。因为在hashmap中使用2的整数幂来作为掩码,所以只在当前掩码之上的位上发生 * 变化的散列总是会发生冲突。(在已知的例子中,Float键的集合在小表中保持连续的整数)因此,我们应用一个位运算 * 来向下转移高位的影响。 这是在综合考虑了运算速度,效用和质量之后的权衡。因为许多常见的散列集合已经合理分布 * (所以不能从扩散中受益),并且因为我们使用树来处理bin中发生的大量碰撞的情况,所以我们尽可能以代价最低的方式 * 对一些位移进行异或运算以减少系统损失, 以及合并由于hashmap容量边界而不会被用于散列运算的最高位的影响。 * * todo 扰动函数 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 可以看出,这里并不是简单的使用了key的hashCode,而是将它的高16位与低16位做了一个异或操作。(“>>>”是无符号右移的意思,即右移的时候左边空出的部分用0填充)这是一个扰动函数,具体效果后面会说明。接下来再看看之前的putval方法: 1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 2 boolean evict) { 3 Node<K,V>[] tab; Node<K,V> p; int n, i; 4 //如果当前table未初始化,则先重新调整大小至初始容量 5 if ((tab = table) == null || (n = tab.length) == 0) 6 n = (tab = resize()).length; 7 //(n-1)& hash 这个地方即根据hash求序号,想了解更多散列相关内容可以查看下一篇 8 if ((p = tab[i = (n - 1) & hash]) == null) 9 //不存在,则新建节点 10 tab[i] = newNode(hash, key, value, null); 11 else { 12 Node<K,V> e; K k; 13 //先找到对应的node 14 if (p.hash == hash && 15 ((k = p.key) == key || (key != null && key.equals(k)))) 16 e = p; 17 else if (p instanceof TreeNode) 18 //如果是树节点,则调用相应的putVal方法,这部分放在第三篇内容里 19 //todo putTreeVal 20 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 21 else { 22 //如果是链表则之间遍历查找 23 for (int binCount = 0; ; ++binCount) { 24 if ((e = p.next) == null) { 25 //如果没有找到则在该链表新建一个节点挂在最后 26 p.next = newNode(hash, key, value, null); 27 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 28 //如果链表长度达到树化的最大长度,则进行树化,该函数内容也放在第三篇 29 //todo treeifyBin 30 treeifyBin(tab, hash); 31 break; 32 } 33 if (e.hash == hash && 34 ((k = e.key) == key || (key != null && key.equals(k)))) 35 break; 36 p = e; 37 } 38 } 39 //如果已存在该key的映射,则将值进行替换 40 if (e != null) { // existing mapping for key 41 V oldValue = e.value; 42 if (!onlyIfAbsent || oldValue == null) 43 e.value = value; 44 afterNodeAccess(e); 45 return oldValue; 46 } 47 } 48 //修改次数加一 49 ++modCount; 50 if (++size > threshold) 51 resize(); 52 afterNodeInsertion(evict); 53 return null; 54 } 注意看第八行的代码: tab[i = (n - 1) & hash] (n - 1) & hash即通过key的hash值来取对应的数组下标,并非是对table的size进行取余操作。 那么,为什么要这样做呢?首先,扰动函数的目的就是为了扩大高位的影响,使得计算出来的数值包含了高 16 位和第 16 位的特性,让 hash 值更加深不可测来降低碰撞的概率。从hash方法的注释中,我们也可以找到答案,一般的散列,其实都是做取余处理,但是HashMap中的table大小是2的整数次幂,也就是说,肯定不是质数,那么在取余的时候,偶数的映射范围势必就要小了一半,这样效果显然就差很多,而且,除法和取余其实是很慢的操作,所以在JDK8中,使用了一种很巧妙的方式来进行散列。首先,table的大小size设置成了2的整数次幂,这样使用size-1就变成了掩码,下面是我找的一张图,能很好的解释这个过程: n是table的大小,默认是16,二进制即为10000,n - 1对应的二进制则为1111,这样再与hash值做“与”操作时,就变成了掩码,除了最后四位全部被置为0,而最后四位的范围肯定会落在(0~n-1)之间,正好是数组的大小范围,散列函数的妙处就在于此了。简直不能更稳,一波操作猛如虎。 那么我们继续上一篇的栗子,我们来一步一步分析一下,小明和小李的hash值的映射过程: 小明的hash值是756692,转换为二进制为10111000101111010100,table的大小是32,n-1=31,对应的二进制为:11111,做“与”运算之后,得到的结果是10100,即为20。 小李的hash值是757012,转换为二进制为10111000110100010100,与11111做与运算后,得到的结果也是10100,即20,于是就与小明发生了冲突,但还是要先来后到,于是小李就挂在了小明后面。 散列函数看完了,我们接下来再看看扩容函数。 扩容函数 扩容函数其实之前也已经见过了,就在上面的putVal方法里,往上面翻一翻,第六行可以看到resize函数,这就是扩容函数,让我们来看看它的庐山真面目: 1 /** 2 * 初始化或将table的大小进行扩容。 如果table为null,则按照字段threshold中的初始容量目标进行分配。 3 * 否则,因为我们使用2次幂进行扩容,所以在新表中,来自每个bin中的元素必须保持在相同的索引处,或者以原偏移量的2次幂进行移动。 4 */ 5 final Node<K,V>[] resize() { 6 Node<K,V>[] oldTab = table; 7 int oldCap = (oldTab == null) ? 0 : oldTab.length; 8 int oldThr = threshold; 9 int newCap, newThr = 0; 10 if (oldCap > 0) { 11 if (oldCap >= MAXIMUM_CAPACITY) { 12 threshold = Integer.MAX_VALUE; 13 return oldTab; 14 } 15 //新的容量扩展成原来的两倍 16 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 17 oldCap >= DEFAULT_INITIAL_CAPACITY) 18 //阈值也调整为原来的两倍 19 newThr = oldThr << 1; // double threshold 20 } 21 else if (oldThr > 0) // initial capacity was placed in threshold 22 newCap = oldThr; 23 else { // zero initial threshold signifies using defaults 24 newCap = DEFAULT_INITIAL_CAPACITY; 25 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 26 } 27 if (newThr == 0) { 28 float ft = (float)newCap * loadFactor; 29 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 30 (int)ft : Integer.MAX_VALUE); 31 } 32 threshold = newThr; 33 @SuppressWarnings({"rawtypes","unchecked"}) 34 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 35 table = newTab; 36 //将旧数组中的node重新散列到新数组中 37 if (oldTab != null) { 38 for (int j = 0; j < oldCap; ++j) { 39 Node<K,V> e; 40 if ((e = oldTab[j]) != null) { 41 oldTab[j] = null; 42 if (e.next == null) 43 newTab[e.hash & (newCap - 1)] = e; 44 else if (e instanceof TreeNode) 45 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 46 else { // preserve order 47 Node<K,V> loHead = null, loTail = null; 48 Node<K,V> hiHead = null, hiTail = null; 49 Node<K,V> next; 50 do { 51 next = e.next; 52 if ((e.hash & oldCap) == 0) { 53 if (loTail == null) 54 loHead = e; 55 else 56 loTail.next = e; 57 loTail = e; 58 } 59 else { 60 if (hiTail == null) 61 hiHead = e; 62 else 63 hiTail.next = e; 64 hiTail = e; 65 } 66 } while ((e = next) != null); 67 if (loTail != null) { 68 loTail.next = null; 69 newTab[j] = loHead; 70 } 71 if (hiTail != null) { 72 hiTail.next = null; 73 newTab[j + oldCap] = hiHead; 74 } 75 } 76 } 77 } 78 } 79 return newTab; 80 } 这里可以看到,如果原来的table还未被初始化的话,调用该函数后就会被扩容到默认大小(16),上一篇中也已经说过,HashMap也是使用了懒加载的方式,在构造函数中并没有初始化table,而是在延迟到了第一次插入元素之后。 当使用put插入元素的时候,如果发现目前的bins占用程度已经超过了Load Factor所设置的比例,那么就会发生resize,简单来说就是把原来的容量和阈值都调整为原来的2倍,之后重新计算index,把节点再放到新的bin中。因为index值的计算与table数组的大小有关,所以扩容后,元素的位置有可能会调整: 以上图为例,如果对应的hash值第五位是0,那么做与操作后,得到的序号不会变,那么它的位置就不会改变,相反,如果是1,那么它的新序号就会变成原来的序号+16,。 好像也不是很多嘛,嗯,算法部分就先介绍到这里了,之后的一篇再来说说HashMap中的EntrySet,KeySet和values(如果时间够的话顺便把迭代器也说一说)。 好了,本篇就此愉快的结束了,最后祝大家端午节快乐!如果觉得内容还不错的话记得动动小手点关注哦,你的支持就是我最大的动力! 真正重要的东西,用眼睛是看不见的。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册