从Langchain到ReAct,在大模型时代下全新的应用开发核心
什么是ReAct框架
关于什么是langchain,可以参考:
https://ata.alibaba-inc.com/articles/266839?spm=ata.23639420.0.0.1dea7536uD7yhh
在使用langchain的过程中,大模型给人留下最深刻的印象无疑是Agent功能。大模型会自己分析问题,选择合适的工具,最终解决问题。这个功能背后的原理就是来自ReAct框架。
ReAct是Reasoning and Acting(也有一说是Reason Act)缩写,意思是LLM可以根据逻辑推理(Reason),构建完整系列行动(Act),从而达成期望目标。LLM灵感来源是人类行为和推理之间的协同关系。人类根据这种协同关系学习新知识,做出决策,然后执行。LLM模型在逻辑推理上有着非常优秀的表现,因此有理由相信LLM模型也可以像人类一样进行逻辑推理,学习知识,做出决策,并执行。在实际使用中,LLM会发生幻觉和错误判断的情况。这是因为LLM在训练的时候接触到的知识有限。因此对超出训练过程中使用的数据进行逻辑分析时,LLM就会开始不懂装懂地编造一些理由。因此对于解决这个问题最好的办法是,可以保证LLM模型在做出分析决策时,必须将应该有的知识提供给LLM。
ReAct方式的作用就是协调LLM模型和外部的信息获取,与其他功能交互。如果说LLM模型是大脑,那ReAct框架就是这个大脑的手脚和五官。同时具备帮助LLM模型获取信息、输出内容与执行决策的能力。对于一个指定的任务目标,ReAct框架会自动补齐LLM应该具备的知识和相关信息,然后再让LLM模型做出决策,并执行LLM的决策。
它是如何运作的
图片来源:https://arxiv.org/abs/2210.03629
一个ReAct流程里,关键是三个概念:
Thought:由LLM模型生成,是LLM产生行为和依据。可以根据LLM的思考,来衡量他要采取的行为是否合理。这是一个可用来判断本次决策是否合理的关键依据。相较于人类,thought的存在可以让LLM的决策变得更加有可解释性和可信度。
Act:Act是指LLM判断本次需要执行的具体行为。Act一般由两部分组成:行为和对象。用编程的说法就是API名称和对应的入参。LLM模型最大的优势是,可以根据Thought的判断,选择需要使用的API并生成需要填入API的参数。从而保证了ReAct框架在执行层面的可行性。
Obs:LLM框架对于外界输入的获取。它就像LLM的五官,将外界的反馈信息同步给LLM模型,协助LLM模型进一步的做分析或者决策。
一个完整的ReAct的行为,包涵以下几个流程:
1.输入目标:任务的起点。可以是用户的手动输入,也可以是依靠触发器(比如系统故障报警)。
2.LOOP:LLM模型开始分析问题需要的步骤(Thought),按步骤执行Act,根据观察到的信息(Obs),循环执行这个过程。直到判断任务目标达成。
3.Finish:任务最终执行成功,返回最终结果。
一个可以自己尝试ReAct过程的Prompt的例子:
You are an assistant, please fully understand the user's question, choose the appropriate tool, and help the user solve the problem step by step.
### CONSTRAINTS ####
1. The tool selected must be one of the tools in the tool list.
2. When unable to find the input for the tool, please adjust immediately and use the AskHumanHelpTool to ask the user for additional parameters.
3. When you believe that you have the final answer and can respond to the user, please use the TaskCompleteTool.
5. You must response in Chinese;
### Tool List ###
[
Search: 如果需要搜索请用它.paramDescription : [{"name": "searchKey", "description": "搜索参数","type":"String"}]
AskHumanHelpTool: 如果需要人类帮助,请使用它。paramDescription : [{"name": "question", "description": "问题","type":"String"}]
TaskCompleteTool:如果你认为你已经有了最终答案,请使用它。paramDescription : [{"name": "answer", "description": "答案","type":"String"}]
]
You should only respond in JSON format as described below
### RESPONSE FORMAT ###
{
{"thought": "为什么选择这个工具的思考","tool_names": "工具名","args_list": {“工具名1”:{"参数名1": "参数值1","参数名2": "参数值2"}}}}
Make sure that the response content you return is all in JSON format and does not contain any extra content.
把它复制粘贴到ChatGPT中,就可以感受一下最初级的ReAct交互的体验。读者自己充当一个ReAct框架,去执行Act和Obs过程。
先说ReAct的缺点
毫无疑问,让LLM模型进行决策不是完美无缺的。目前有几个显而易见的缺点是工程上必须要考虑的。
1、LLM模型的通病
图1
图2
数据来源:https://arxiv.org/abs/2210.03629
注:表中CoT一栏是指模型只进行链式思考,不从外部获取必要信息。本文不对ReAct和CoT的对比做分析。想了解的可以自行查看论文。
LLM模型最大的问题是产出内容不稳定。这种不稳定不仅仅是内容存在波动,也体现在他对复杂问题的分析,解决上存在一定的波动。
上图论文中,采用PaLM-540B模型进行的测试。图1中可以看到采用ReAct模式时,LLM模型在知识密集型推理任务如问答 (HotPotQA)上表现存在不足,而在事实验证(Fever)上有着更好的表现。这个主要原因是采用ReAct方式会约束LLM模型制定推理方面的灵活性。LLM擅长逻辑推理,但过多非信息性的搜索有可能会阻碍模型推理,导致难以恢复推理流程。
图2中的数据也可以看到,即使采用ReAct模型,也会有幻觉、不知道改搜索什么内容,错误归因等现象发生。
LLM的表现来看,更像一个人类。泛用性很强,可以通过自己思考去解决很多问题,但也会因为自身知识,能力上的缺陷无法做到稳定输出。但LLM跟人比,会显得更加盲目自信,对于不了解不理解的问题也会编造一些内容(幻觉)。
论文中采用的还是PaLM-540B模型,整体表现是明显差于GPT4.0和更新的模型。我们有理由相信未来的模型可以把幻觉,不稳定输出等问题发生概率降低到最低。但现阶段,如果应用LLM模型的时候必须要考虑这个问题。
2、成本
成本是目前使用LLM模型无法绕过的问题。每一个经常使用LLM模型的开发者,都会觉得自己仿佛回到了20年前开发单片机一样,提交给LLM的内容里每一个字节都需要思考。按照输入和输出token数量收费方式,让开发者必须斟酌内容和内容带来的价值能否cover住成本。
而采用ReAct方式,开发者是无法控制输入内容的。因为在任务提交给LLM后,LLM对任务的拆解、循环次数是不可控的。因此存在一个可能性,过于复杂的任务导致Token过量消耗。一个复杂任务一晚上跑掉一栋别墅的钱也不是玩笑话。
3、响应时间
比起大部分API接口毫秒级的响应,LLM响应时间是秒级以上。以ChatGPT的API为例,普通一次Completion接口,响应时间都要10秒以上。如果是复杂的任务,达到20秒以上也是可能的。当然,这里不确定是不是OpenAI本身工程能力有限导致的,还是LLM本身就需要这么长的时间。
在ReAct模式下,这个时间变得更加不可控。因为无法确定需要拆分多少步骤,需要访问多少次LLM模型。因此在在秒级接口响应的背景下,做成同步接口显然是不合适的,需要采用异步的方式。而异步方式,又会影响用户体验,对应用场景的选择又造成了限制。
为什么需要ReAct框架
通过前面描述,应该看得出来ReAct本身是使用LLM模型的一种方式。但在往工程化上应用的时候存在各种问题。因此,基于这些工程上的问题,我在思考除去等待LLM模型自身性能提升外,我们是否可以通过一些工程化的手段去规避或者解决其中一些问题。
Langchain、AutoGPT or GPT PlugIn or ?
目前开源环境下已经有Langchain,AutoGPT这些采用ReAct方式的框架。Openai也通过Plugin的方式提供出一些基于ReAct方式的应用。
前者是基于Python代码写的ReAct框架。毫无疑问在接入Java应用中会存在很多问题。不论是定制Agent可扩展的Tool的能力,还是运维部署都会比较麻烦。当应用出问题时,debug更加困难。高兴的是集团已经有兄弟开始逐步实现Java版本的langchain:https://ata.alibaba-inc.com/articles/267337?spm=ata.23639420.0.0.1dea7536qqQ99i#SB_szwZT
但是,我认为langchain只是ReAct框架的第一步。分析Langchain的代码就可以看出,它不是一个以ReAct方式为核心而设计的模型。在上述提到的工程化问题里,并没有很好的解决。所以在我们做Java版本的Langchain的时候,设计的核心应该是ReAct方式。利用Java强大的工程能力,赋能给更多的Java应用。我对ReAct框架的期望是可以构建像Spring一样的生态,让更多不懂算法的Java工程师,也能让自己的应用变得更加智能。
GPT Plugin是采用的ReAct方式,极大的增强了ChatGPT的能力。但它注定是不够灵活,受限于ChatGPT的使用场景。当我的应用需要独立于ChatGPT以外的使用场景时,就很难使用。这里可以认为它是对LLM模型能力的补充,带并不会取代ReAct框架在工程中的使用。但Plugin的优势是集成在LLM模型内部,可以大幅降低ReAct交互次数。从而带来成本降低,响应时间提升的优势。
ReAct框架需要哪些能力
1.稳定的输入和输出
前文所说,现阶段LLM模型的产出是不稳定的。通过对Prompt模板的不断调优,SDK参数调整,可以将LLM产出的内容尽可能的稳定产出。这里的稳定是指对输出内容满足开发者的期望。对于需要LLM模型创作的场景,依然需要支持它可以自由不设限的,多样化的创作。
对于LLM错误的产出,可以及时发现并提示开发者或者重新向LLM模型查询,获取更好更优质的回答。
2.成本优化和监控
构建向量缓存,对于有通用回复的内容可以通过缓存的形式减少对LLM交互的次数。
优化Prompt模板,减少无味的Token消耗
对于已有Token的消耗需要有可视化的监控。一方面可以洞察消耗情况,另一方面也可以做到成本预警。可以对还未执行的任务,进行成本预估。
3.优化LLM模型响应时间
减少LLM交互次数,可以通过MapReduce的方式提高并发执行交互的能力。
4.本地知识库的导入和动态embedding
对于LLM决策需要的额外了解的信息,可以动态的嵌入到每一次Prompt内容中。从而提升LLM模型决策的准确性。
5.能力快速接入和可拓展
对于ReAct应用,LLM模型决策是一方面,更主要的是LLM决策后,能做啥。这里需要有一个清晰的能力定义与描述以及快速接入能力。可以方便LLM模型理解能力并在需要的时候使用它。LLM框架能评估每个能力的描述,方便开发者更好的将新能力或者已有的能力接入LLM模型中。
6.And more……
目前还是我在写ReAct框架中思考需要解决的问题,后续还有更多的问题需要解决会逐步补充进来。
后续
ReAct框架可以让现有应用得到一次智能化的进化的机会。以前需要人工编排服务调用链路会成为历史。LLM可以让我们的应用开发成本大幅下降,也可以让软件设计模式得到全新的变化。也许未来有一天,什么设计模式,什么DDD都会成为历史。写好一个个函数,剩下的交给大模型吧!
设计模式总结(四):设计原则
1.单一职责原则2.开放封闭原则3.里式替换原则4.接口分离原则客户端需要什么接口就是提供什么接口,把不需要的接口剔除,这就要求对接口进行细化,保证其纯洁性。5.依赖倒置原则
带你读《存储漫谈:Ceph原理与实践-样章》——2.1.3 鹿死谁手,犹未可知
2.1.3? 鹿死谁手,犹未可知在大型系统设计中,经常会看到一种“三十年河东,三十年河西”的反差现象。举个例子,在早期的系统开发中,为了简化应用开发者对系统操作、数据操作的复杂度,人们抽象出了操作系统和文件系统这些概念,而随着近些年底层开发者对性能越来越极致的追求,越来越多的系统开始采用 kernel-bypass、去文件系统等设计理念。类似地,在近 10 年对去中心化设计思潮的追求之后,似乎越来越多的系统又走回了中心化设计的道路上。比较有代表性的是微软的 Azure Storage 和阿里巴巴的盘古存储系统,两者都是对 GFS 这一模型的延伸和强化,它们都在海量的数据和业务下得到了验证,是适合超大规模存储系统使用的设计模式。
带你读《自主管理身份:分布式数字身份和可验证凭证》——关于本书
关于本书欢迎阅读《自主管理身份:分布式数字身份和可验证凭证》!本书的首要目的是向读者介绍 SSI 的基本概念,帮助读者清晰理解为何我们当前已经处于互联网身份演变的分水岭。为达到这一目的,本书除了介绍笔者自己的观点,还汇集了世界各地领先 SSI 专家的观点。他们分享了 SSI 在各个领域,如技术、商业、法律、社会甚至哲学领域产生的影响。本书还介绍了如何应用 SSI 解决实际市场问题的具体示例,以帮助读者了解如何将 SSI应用于工作、学习和生活中。笔者也希望这本书能够启发其他相关人员展开相关讨论,并启发社会各界关注相关观点。目标读者笔者在撰写本书时秉持的理念是向开发人员、产品经理和商业领导人全面介绍这项全新的基础技术,帮助他们拓宽视野,掌握全局,了解跨学科发展趋势,并在工作中充分应对即将发生的重大市场变革。SSI 需要各方齐心协力、各尽其职、各扬所长,塑造我们理想中的未来。本书的目标读者非常广泛,他们都能从书中找到感兴趣的内容。◎ 架构师和开发人员。◎ 产品经理。◎ 用户体验(UX)设计师。◎ 商业和政府领导人。◎ 法律专业人士。◎ 隐私、去中心化和区块链技术关注者。因此,我们将本书分为 4 个部分。第 1 部分对 SSI 进行全面介绍,主要内容包括 SSI 技术的起源、技术原理、主要特征和优点。这一部分面向所有对 SSI 感兴趣的读者。第 2 部分专为想要深入了解 SSI 架构的专业技术人员设计,包括 SSI 架构的主要组成要素和设计模式,但并未深入代码级别。第 3 部分从另一个角度切入,重点介绍 SSI 的文化和哲学渊源,以及 SSI 对互联网和社会的最终影响。这部分特别适合对隐私保护感兴趣,以及想要了解 SSI 起源的读者。第 4 部分邀请各行各业专家分享他们所处的垂直市场如何应用 SSI 技术,探讨 SSI 对企业和政府的意义。这一部分尤其适合架构师和产品经理,因为他们需要向商业领导人证明为 SSI 技术对他们的业务部门具有重要影响—不管是机遇、威胁,还是破坏。第 1 部分全面介绍了自主管理身份(SSI)的基础,包括它从哪里来、如何工作,以及主要特性和优点。笔者建议读者按顺序阅读第 1 部分各章节,它们适用于所有对 SSI 感兴趣的人,无论你关注的是技术、产品、业务,还是政策。第 2 部分深入探讨 SSI 技术,面向希望了解 SSI 技术原理的读者。这部分并未深入代码级别(除了第 7 章和第 8 章中的代码示例),但涵盖了 SSI 架构的所有主要方面,能够为架构师、开发人员、系统管理员,以及任何想了解 SSI“栈”的人提供全面的技术介绍。第 3 部分将 SSI 视为一项跨越传统行业边界,涵盖了更广泛的技术、法律、社会基础设施领域的话题;探讨作为 SSI 基础的去中心化技术如何推动哲学、社会和文化的更大变革;从历史、政治、社会角度讨论哪些技术属于 SSI 技术,哪些技术不属于 SSI 技术,以及区分理由。这部分内容面向所有读者,但如果你主要关注 SSI 技术或业务解决方案,你也可以略过这一部分。第 4 部分探讨 SSI 将如何影响各类企业、行业和政府部门—各章内容由各个垂直行业的专家撰写。大多数章节结尾附带了一个 SSI 记分卡汇总表(第 4 章对其进行了说明),以评估 SSI 对特定垂直市场的影响。最后,我们还编制了 5 个附录,为读者提供更多工具和视角,协助读者进一步探索SSI,附录主要内容如下。◎ 附录 A—简要介绍本书 liveBook 版本中收录的 11 个附加章节,继续第 4 部分的工作,通过各个垂直市场专家分享的看法探讨 SSI 在垂直市场中的应用。◎ 附录 B—介绍网络上发布的关于 SSI 的著名文章,这些文章深入探讨了关于 SSI 和分布式数字信任基础设施的特殊主题。◎ 附录 C—介绍 Christopher Allen 所著 The Path to Self-Sovereign Identity。其是由安全套接层(SSL)协议合著者撰写的关于 SSI 的具有里程碑意义的文章原文。SSL 协议实现了网络加密标准化。◎ 附录 D—介绍 Fabian Vogelsteller 和 Oliver Terbu 所著 Identity in the Ethereum Blockchain Ecosystem。这是另一篇关于 SSI 的具有里程碑意义的文章,作者分别是以太坊生态系统中著名的开发人员和 ConsenSys 的身份产品负责人。◎ 附录 E—介绍 The Principles of SSI。在本书的最后,笔者列出了由 Sovrin 基金会主持的一个全球社区项目开发的 12 项 SSI 基本原则,这套原则于 2020 年 12 月以 15 种语言出版。关于代码本书的技术章节主要集中在第 2 部分。SSI 的架构和设计选择非常广泛,因此,本书大部分内容并未深入代码级别,除了第 7 章中关于 JavaScript 对象表示法(JSON)和 JSON-LD 可验证凭证的示例,以及第 8 章中关于 DID 和 DID 文件的示例。但是,本书也多次提到在世界各地开展的包含 SSI 要素的重大开源项目,而且大部分项目都可公开访问。liveBook 论坛购买本书的读者可以免费访问由曼宁(Manning)出版社运营的一个私人网络论坛(Manning 出版社官网的 liveBook 论坛),读者可以在该论坛上发表对本书的评论,提出技术问题,并且有机会获得本书作者和其他用户的帮助。Manning 出版社承诺为读者提供一个平台,方便读者与本书作者进行有意义的对话。但Manning 出版社无法保证本书作者的参与度,本书作者的参与基于自愿原则(并且是无偿的)。我们建议读者提出具有挑战性的问题,以吸引本书作者的关注!本书在售期间,读者可以随时前往 Manning 出版社网站访问论坛并查看之前的讨论。其他线上资源本书所有章节都列出了用于拓展阅读的参考资料。笔者特别推荐读者阅读第 14 章的参考资料,这些资料介绍了互联网身份和 SSI 社区的演变历程。纵观全书,每当有相应章节的更多信息和网络研讨会时,笔者都包含了对 SSI Meetup网络研讨会的参考。读者可以在 IdentityBook.info 上注册,以获取与本书相关的最新内容。笔者特别建议读者关注以下社区以了解 SSI 领域的最新进展:◎ W3C 可验证凭证工作组;◎ W3C 分布式身份工作组;◎ W3C 凭证社区工作组;◎ 分布式身份基金会(DIF);◎ Sovrin 基金会;◎ ToIP 基金会;◎ Covid-19 证书倡议。
别再夹灰了!这份阿里巴巴Java架构六大专题面试宝典值得你刷一刷
Java面试是一个老生常谈的问题。每年到了金三银四&金九银十这种跳槽黄金季就会有一大批程序员出来面试找工作。流程就是熟悉的网上开始找面试题,面试手册,面试宝典,一收藏就是一大把,看到什么都觉得Nice,看几眼之后就收藏夹吃灰,对面试其实起不到什么实际性帮助。但其实只要你不做收藏怪,好好地刷一刷,面试前做好准备,面试后做好复盘,不存在拿不到理想的Offer退而求其次这种情况。这不马上到金三银四了,已经有不少小伙伴出去面试过了,跟我反馈的效果都不是很理想,也有很多粉丝后台私信我说想要一份最新的,高质量的面试突击手册,金三银四想冲一冲。LZ前段时间也是偶然得到这份阿里巴巴Java架构六大核心专题面试宝典,昨天看了下,内容还是挺全的,都是些大厂的面试真题汇总。文档涵盖了:大数据与高并发、分布式、中间件、数据库、设计模式与实践、数据结构与算法六大内容,内容技术水平对标阿里P7级别,相比于网上一些杂乱无章的涵盖太多旧题,错题,基础题的面试题总结,这份阿里内部的面试宝典简直不要太香!如果最近有小伙伴有面试需求,需要完整版一看,可以点击此处来获取就可以了! 阿里巴巴Java架构六大核心专题面试宝典首先就是大型分布式网站构建使用反向代理和CDN加速网站响应分布式文件系统和分布式数据库系统NoSQL和搜索引擎分布式一致性理论paxos、raft、 zab算法接下来是中间件缓存的内容然后是大数据高并发相关内容这个模块从零到一地给大家展示了一个高并发秒杀系统的搭建过程,对架构师朋友们应该会有帮助技术难点架构设计思想大型互联网应用(大数据、高并发、多样数据类型)的难点和解决方案常见的限流算法数据库开发并发事务带来的问题事务隔离级别及锁的实现机制MvCC(多版本并发控制)间隙锁与幻读设计模式实践基于RBAC的权限管理角色访问控制(RBAC)执行流程分析日志记录是最佳实践算法底层Topk问题资源池思想JVM内存管理算法容器虚拟化技术,Doocker思想持续集成、持续发布,jenkins内容节选redis的过期策略以及内存淘汰机制搜索引擎应用场景数据库类型&商品信息存放索引实现机制可达性分析算法面试题举例最后为了不影响大家的阅读体验,就不把篇幅继续拉长了,需要面试宝典完整的小伙伴可以点击此处来获取就可以了!
Java 容器详解:使用与案例
Java容器是一套工具,用于存储数据和对象。可以与C++的STL类比。Java容器也称为Java Collection Framework (JCF)。除了存储对象的容器之外,还提供了一套工具类,用于处理和操作容器中的对象。总体来说,这是一个框架,它包含了Java对象容器和工具类。一、概览容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。Collection1. SetTreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。LinkedHashSet:具有 HashSet 的查找效率,并且内部使用双向链表维护元素的插入顺序。2. ListArrayList:基于动态数组实现,支持随机访问。Vector:和 ArrayList 类似,但它是线程安全的。LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。3. QueueLinkedList:可以用它来实现双向队列。PriorityQueue:基于堆结构实现,可以用它来实现优先队列。MapTreeMap:基于红黑树实现。HashMap:基于哈希表实现。HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程同时写入 HashTable 不会导致数据不一致。它是遗留类,不应该去使用它,而是使用 ConcurrentHashMap 来支持线程安全,ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。二、容器中的设计模式迭代器模式Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
for (String item : list) {
System.out.println(item);
}适配器模式java.util.Arrays#asList() 可以把数组类型转换为 List 类型。@SafeVarargs
public static <T> List<T> asList(T... a)应该注意的是 asList() 的参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组。Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr);也可以使用以下方式调用 asList():List list = Arrays.asList(1, 2, 3);三、源码分析如果没有特别说明,以下源码分析基于 JDK 1.8。在 IDEA 中 double shift 调出 Search EveryWhere,查找源码文件,找到之后就可以阅读源码。ArrayList1. 概览因为 ArrayList 是基于数组实现的,所以支持快速随机访问。RandomAccess 接口标识着该类支持快速随机访问。public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable数组的默认大小为 10。private static final int DEFAULT_CAPACITY = 10;2. 扩容添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),即 oldCapacity+oldCapacity/2。其中 oldCapacity >> 1 需要取整,所以新容量大约是旧容量的 1.5 倍左右。(oldCapacity 为偶数就是 1.5 倍,为奇数就是 1.5 倍-0.5)扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}3. 删除元素需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的。public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null;
return oldValue;
}4. 序列化ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。transient Object[] elementData;ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
s.defaultReadObject();
s.readInt();
if (size > 0) {
ensureCapacityInternal(size);
Object[] a = elementData;
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
int expectedModCount = modCount;
s.defaultWriteObject();
s.writeInt(size);
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);5. Fail-FastmodCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。代码参考上节序列化中的 writeObject() 方法。Vector1. 同步它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}2. 扩容Vector 的构造函数可以传入 capacityIncrement 参数,它的作用是在扩容时使容量 capacity 增长 capacityIncrement。如果这个参数的值小于等于 0,扩容时每次都令 capacity 为原来的两倍。public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}private void grow(int minCapacity) {
int oldCapacity = elementData.length;
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);
}调用没有 capacityIncrement 的构造函数时,capacityIncrement 值被设置为 0,也就是说默认情况下 Vector 每次扩容时容量都会翻倍。public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector() {
this(10);
}3. 与 ArrayList 的比较Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍。4. 替代方案可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。List<String> list = new CopyOnWriteArrayList<>();CopyOnWriteArrayList1. 读写分离写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。写操作需要加锁,防止并发写入时导致写入数据丢失。写操作结束之后需要把原始数组指向新的复制数组。public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}2. 适用场景CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。但是 CopyOnWriteArrayList 有其缺陷:内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。LinkedList1. 概览基于双向链表实现,使用 Node 存储链表节点信息。private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}每个链表存储了 first 和 last 指针:transient Node<E> first;
transient Node<E> last;2. 与 ArrayList 的比较ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。ArrayList 和 LinkedList 的区别可以归结为数组和链表的区别:数组支持随机访问,但插入删除的代价很高,需要移动大量元素;链表不支持随机访问,但插入删除只需要改变指针。HashMap为了便于理解,以下源码分析以 JDK 1.7 为主。1. 存储结构内部包含了一个 Entry 类型的数组 table。Entry 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶取模运算结果相同的 Entry。transient Entry[] table;static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
}2. 拉链法的工作原理HashMap<String, String> map = new HashMap<>();
map.put("K1", "V1");
map.put("K2", "V2");
map.put("K3", "V3");新建一个 HashMap,默认大小为 16;插入 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。插入 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。插入 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 前面。应该注意到链表的插入是以头插法方式进行的,例如上面的 不是插在 后面,而是插入在链表头部。查找需要分成两步进行:计算键值对所在的桶;在链表上顺序查找,时间复杂度显然和链表的长度成正比。3. put 操作public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 键为 null 单独处理
if (key == null)
return putForNullKey(value);
int hash = hash(key);
// 确定桶下标
int i = indexFor(hash, table.length);
// 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 插入新键值对
addEntry(hash, key, value, i);
return null;
}HashMap 允许插入键为 null 的键值对。但是因为无法调用 null 的 hashCode() 方法,也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。void addEntry(int hash, K key, V value, int bucketIndex) {
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);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
// 头插法,链表头部指向新的键值对
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}4. 确定桶下标很多操作都需要先确定一个键值对所在的桶下标。int hash = hash(key);
int i = indexFor(hash, table.length);4.1 计算 hash 值final int hash(Object k) {
int h = hashSeed;
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);
}public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}4.2 取模令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:x : 00010000
x-1 : 00001111令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:y : 10110010
x-1 : 00001111
y&(x-1) : 00000010这个性质和 y 对 x 取模效果是一样的:y : 10110010
x : 00010000
y%x : 00000010我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时用位运算的话能带来更高的性能。确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的 n 次方,那么就可以将这个操作转换为位运算。static int indexFor(int h, int length) {
return h & (length-1);
}5. 扩容-基本原理设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此查找的复杂度为 O(N/M)。为了让查找的成本降低,应该使 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。参数含义capacitytable 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。size键值对数量。thresholdsize 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。loadFactor装载因子,table 能够使用的比例,threshold = (int)(capacity* loadFactor)。static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry[] table;
transient int size;
int threshold;
final float loadFactor;
transient int modCount;从下面的添加元素代码中可以看出,当需要扩容时,令 capacity 为原来的两倍。void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}6. 扩容-重新计算桶下标在进行扩容时,需要把键值对重新计算桶下标,从而放到对应的桶上。在前面提到,HashMap 使用 hash%capacity 来确定桶下标。HashMap capacity 为 2 的 n 次方这一特点能够极大降低重新计算桶下标操作的复杂度。假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32:capacity : 00010000
new capacity : 00100000对于一个 Key,它的哈希值 hash 在第 5 位:为 0,那么 hash%00010000 = hash%00100000,桶位置和原来一致;为 1,hash%00010000 = hash%00100000 + 16,桶位置是原位置 + 16。7. 计算数组容量HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。先考虑如何求一个数的掩码,对于 10010000,它的掩码为 11111111,可以使用以下方法得到:mask |= mask >> 1 11011000
mask |= mask >> 2 11111110
mask |= mask >> 4 11111111mask+1 是大于原始数字的最小的 2 的 n 次方。num 10010000
mask+1 100000000以下是 HashMap 中计算数组容量的代码:static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}8. 链表转红黑树从 JDK 1.8 开始,一个桶存储的链表长度大于等于 8 时会将链表转换为红黑树。9. 与 Hashtable 的比较Hashtable 使用 synchronized 来进行同步。HashMap 可以插入键为 null 的 Entry。HashMap 的迭代器是 fail-fast 迭代器。HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。ConcurrentHashMap1. 存储结构static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。Segment 继承自 ReentrantLock。static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
static final int MAX_SCAN_RETRIES =
Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
}final Segment<K,V>[] segments;默认的并发级别为 16,也就是说默认创建 16 个 Segment。static final int DEFAULT_CONCURRENCY_LEVEL = 16;2. size 操作每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。transient int count;在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。static final int RETRIES_BEFORE_LOCK = 2;
public int size() {
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow;
long sum;
long last = 0L;
int retries = -1;
try {
for (;;) {
// 超过尝试次数,则对每个 Segment 加锁
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock();
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
// 连续两次得到的结果一致,则认为这个结果是正确的
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}3. JDK 1.8 的改动JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发度与 Segment 数量相等。JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。LinkedHashMap存储结构继承自 HashMap,因此具有和 HashMap 一样的快速查找特性。public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;accessOrder 决定了顺序,默认为 false,此时维护的是插入顺序。final boolean accessOrder;LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }afterNodeAccess()当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}afterNodeInsertion()在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。evict 只有在构建 Map 的时候才为 false,在这里为 true。void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}LRU 缓存以下是使用 LinkedHashMap 实现的一个 LRU 缓存:设定最大缓存空间 MAX_ENTRIES 为 3;使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。class LRUCache<K, V> extends LinkedHashMap<K, V> {
private static final int MAX_ENTRIES = 3;
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
LRUCache() {
super(MAX_ENTRIES, 0.75f, true);
}
}public static void main(String[] args) {
LRUCache<Integer, String> cache = new LRUCache<>();
cache.put(1, "a");
cache.put(2, "b");
cache.put(3, "c");
cache.get(1);
cache.put(4, "d");
System.out.println(cache.keySet());
}[3, 1, 4]WeakHashMap存储结构WeakHashMap 的 Entry 继承自 WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收。private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>ConcurrentCacheTomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。ConcurrentCache 采取的是分代缓存:经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收。当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。public final class ConcurrentCache<K, V> {
private final int size;
private final Map<K, V> eden;
private final Map<K, V> longterm;
public ConcurrentCache(int size) {
this.size = size;
this.eden = new ConcurrentHashMap<>(size);
this.longterm = new WeakHashMap<>(size);
}
public V get(K k) {
V v = this.eden.get(k);
if (v == null) {
v = this.longterm.get(k);
if (v != null)
this.eden.put(k, v);
}
return v;
}
public void put(K k, V v) {
if (this.eden.size() >= size) {
this.longterm.putAll(this.eden);
this.eden.clear();
}
this.eden.put(k, v);
}
}总结本文介绍了Java容器的基本知识,其中包括容器的使用方法和注意事项。虽然这些知识已经足够入门,但要真正掌握Java容器,建议深入了解容器的内部实现方式。建议多查阅Java容器的API和源码,学习容器的算法和数据结构。当你能够自己动手实现这些容器时,才能真正掌握Java容器,并在使用Java容器时更加得心应手。在学习Java容器时,需要注意的是,不同的容器适用于不同的场景,我们需要根据自己的实际需求选择合适的容器。总之,学习Java容器是Java开发者必备的技能之一,只有掌握了Java容器的使用和实现方式,才能在开发中更加得心应手,提高开发效率和代码质量。最后为了方便其他设备和平台的小伙伴观看往期文章,链接奉上:牛客,知乎,开源中国,CSDN,思否,掘金,InfoQ,简书,博客园,慕课,51CTO,helloworld,腾讯开发者社区,阿里开发者社区看完如果觉得有帮助,帮忙点个赞👍
C++开发者必读经典书籍推荐
如果你正在学习C++,那么一本好的教材或参考书可以事半功倍。以下是几本我个人推荐的C++书籍或视频:C++基础看书C++ PrimerC++程序设计语言Effective C++More Effective C++Effective STLSTL源码分析深度探索C++对象模型看视频黑马程序员(B站)C++内存管理(候捷)STL源码分析(候捷)C++ STL与泛型编程高级(候捷)C++11 新特性(候捷)C++进阶书籍C++语言的设计与演化C++沉思录C++ TemplatesC++ 模版元编程视频CppCon数据结构与算法书籍大话数据结构算法图解数据结构与算法分析算法第4版算法导论视频浙大数据结构网课刷题LeetCode操作系统深入理解计算机系统操作系统精髓与设计原理现代操作系统(选读)程序员的自我修养LinuxLinux/UNIX系统编程手册Linux内核设计与实现深入理解Linux内核计算机网络计算机网络自顶向下TCP/IP详解:卷1网络是怎样连接的图解HTTP网络编程Unix网络编程Unix环境高级编程Linux多线程服务器端编程数据库数据库系统概念mysql必知必会高性能MySQLMqSQL技术内幕设计模式大话设计模式Head First设计模式其他Redis设计与实现总结以上是我个人推荐的几本C++书籍。希望这些推荐对你有所帮助!最后为了方便其他设备和平台的小伙伴观看往期文章,链接奉上:牛客,知乎,开源中国,CSDN,思否,掘金,InfoQ,简书,博客园,慕课,51CTO,helloworld,腾讯开发者社区,阿里开发者社区看完如果觉得有帮助,帮忙点个赞👍
被裁后半月面试8家公司无果,凭借这份Java面试指南成功入职阿里
前言上个月班上的好好的突然被通知"毕业了",现在工作也确实不好找。之前近一个月面了很多大大小小的公司降薪太严重都没考虑去,最后没办法本来都打算随便去一家了却偶然得到一个阿里的面试机会,足足面了七面(我太难了)因为我的工程项目经验基本为0 所以被死磕Java,下面我简单说下面试经过:一面: 90mins (基础知识与集合框架)二面:40mins (线程池设计模式等)三面coding:2h(千万级数据量的list找一个数据)四面:50mins(主要调优问JVM)五面coding:2h(百万级int数据量array求和)六面交叉面:20mins(深挖项目)七面hr:20min(聊人生)这一套组合拳打下来所幸我扛住了,周围有很多朋友都问我咋备战面试的,其实我就是每次面试都会把自己面试没答好的一些点简单的复盘总结,并在文档中随手记录,常年累积下来也不知不觉码了206页了,每次面试之前我都会按着文档大概梳理下思路,对我来说挺有用的,能相对提高我的面试通过率,周围很多朋友也都找我要这份“面试复盘笔记”大概涵盖了:大数据与高并发、分布式、中间件、数据库、设计模式与实践、数据结构与算法六大内容,要是有跟我一样今年被迫“毕业”或者正在面试的好兄弟,认识下,发你笔记。笔记一共有206页,篇幅限制无法将所有内容全部展示出来,下面只展示部分内容,需要的小伙伴可以点击此处来获取就可以了! Java面试复盘笔记分布式中间件高并发数据库设计模式算法面试题举例分布式环境下全局唯一的发号器带有过期时间的RU缓存分布式锁分布式环境下的统一配置中心最后需要的小伙伴可以点击此处来获取就可以了!
Rust 笔记、设计模式发布订阅模式及其 在 Rust 语言中的使用
Rust 笔记、设计模式发布订阅模式及其 在 Rust 语言中的使用作者:李俊才 (jcLee95):https://blog.csdn.net/qq_28550263?spm=1001.2101.3001.5343邮箱 :291148484@163.com本文地址:https://blog.csdn.net/qq_28550263/article/details/130877457【介绍】:本文介绍发布订阅模式的相关思想,以及第三方模块 EventEmitter 的使用。推荐阅读:《发布订阅模式原理及其应用(多种语言实现)》 这篇是我早前的博客,里面使用了 Powershell、Dart、Python、TypeScript 讲解或实现了一个 EventEmitter 对象。但是当时还没有考虑使用 Rust。本文多数内容直接来源于该博文,主要是将语言替换成了 Rust。上一节:《?Rust 文件 I/O 的使用?》?|?下一节:《 有限状态机原理及其在Rust 语言中的应用?》目 录1. 引例:从我的一个经历说起1.1 从 订阅 到 发布1.2 如果我不想继续订阅了2. 发布-订阅 的 实践、应用、思考2.1 实践:用 Rust 来复现上面的场景3. 通用型发布者对象的改进3.1 从 Subscriber 的服务员 到 事件的发布者3.2 一个比较初步的功能增强4. 使用现成的第三方模块:EventEmitter4.1 EventEmitter 的安装4.2 在你的项目中使用 EventEmitter4.3 EventEmitter 实例上的方法4.3.1 set_max_listeners 方法4.3.2 set_max_listeners 方法4.3.3 on 方法4.3.4 add_listener 方法4.3.5 off 方法4.3.6 remove_listener 方法4.3.7 emit 方法4.3.8 remove_all_listeners 方法4.3.9 prepend_listener 方法4.3.10 listeners 方法4.3.11 listener_count 方法1. 引例:从我的一个经历说起1.1 从 订阅 到 发布记得一九年的时候我刚刚来到深圳工作,众所周知那时候还没有爆发 新冠疫情,身边的同事们组队去香港购物是常有的事情。但是那会儿我还没有办理港澳通行证,于是年底回老家的时候去当地政务中心办理了。办证是需要时间的,万万没想到的是二零年春节前夕——新冠疫情爆发了。当我回到深圳后的某一天接到老家政务中心的电话,通知我由于疫情的原因,通信证的办理已经被暂停了并且什么时候恢复办理还不能确定,如果愿意等待,则需要到恢复办证的时候,再通知我们。—— 这就是一个 发布-订阅模式的典型例子。发布-订阅 模式 模式中的多方可以分为两类,一类是消息的 发布者,另外一类是消息的 订阅者。在上面的案例中,政务中心的工作人员就是 发布者,当我表示愿意等到恢复通信证办理时,我就 向发布者订阅了 恢复办理的通知(消息),因此我时消息的 订阅者。这样有什么好处呢:对于我(订阅者)来说,不需要每隔几天就打电话到政务服务中心(发布者)去询问是否恢复办理的消息;对于政务服务中心(发布者)同样也不需要每天回答相同的问题——毕竟何时恢复办理他们也不能确定。一旦恢复办理,政务服务中心(发布者)可以一次性地通知所有和我一样地广大订阅者。看到了吗——相比于我们去轮询以获取消息,改用发布-订阅 模式 同时节省了我们双方地时间!多么棒地思想!——运用于程序设计中岂不秒哉?1.2 如果我不想继续订阅了有一种情况也是非常常见的,那就是我不愿意继续等待消息了,也有可能是这个消息对我来说已经不重要了。这时我不再希望继续收到来自发布者的恢复办理通知,那就需要 退订。还记得吗——当我们订阅的时候,是将我们的订阅意愿登记在发布者那边的,这样就能实现发布者在适当的时候通过查询 所有的登记记录 然后逐一通知。因此如果一旦有用户需要退订,其实很简单,只需要订阅者在他们所登记订阅的“订阅者登陆表”中将订阅信息删除掉即可,这样下一次广播通知的时候就不会再将消息发送给退订的用户。2. 发布-订阅 的 实践、应用、思考2.1 实践:用 Rust 来复现上面的场景如果现在你好像明白 发布-订阅模式 的基本思想了——那么就请成热打铁,跟着我用程序来模拟一下证件办理的情景。use std::collections::HashSet;
#[derive(Eq, Hash, PartialEq, Clone)]
struct Subscriber {
name: String,
}
struct Publisher {
subscribers: HashSet<Subscriber>,
name: String,
}
impl Publisher {
fn new(name: &str) -> Publisher {
Publisher {
subscribers: HashSet::new(),
name: name.to_string(),
}
}
fn add_subscriber(&mut self, subscriber: &Subscriber) {
self.subscribers.insert(subscriber.clone());
}
fn remove_subscriber(&mut self, subscriber: &Subscriber) {
self.subscribers.remove(subscriber);
println!("\n=> {} 已取消订阅。\n", subscriber.name);
}
fn notify_all(&self, arg: &str) {
for subscriber in &self.subscribers {
subscriber.notify(self, arg);
}
}
}
impl Subscriber {
fn new(name: &str) -> Subscriber {
Subscriber {
name: name.to_string(),
}
}
fn notify(&self, publisher: &Publisher, arg: &str) {
println!(
"\"{}\"(订阅者) 收到的通知来自 \"{}\"(发布者)的通知: {}",
self.name, publisher.name, arg
);
}
}
fn main() {
let mut publisher = Publisher::new("政务服务中心");
let jack_lee = Subscriber::new("jackLee");
let jack_ma = Subscriber::new("jackMa");
publisher.add_subscriber(&jack_lee);
publisher.add_subscriber(&jack_ma);
println!("------- 第一次发布消息 -------");
publisher.notify_all("[通知] 恢复证件办理!");
// 用户 jackMa 取消订阅
publisher.remove_subscriber(&jack_ma);
println!("------- 第二次发布消息 -------");
publisher.notify_all("[通知] 恢复证件办理!");
}运行该脚本,输出如下:------- 第一次发布消息 -------
"jackLee"(订阅者) 收到的通知来自 "政务服务中心"(发布者)的通知: [通知] 恢复证件办理!
"jackMa"(订阅者) 收到的通知来自 "政务服务中心"(发布者)的通知: [通 知] 恢复证件办理!
=> jackMa 已取消订阅。
------- 第二次发布消息 -------
"jackLee"(订阅者) 收到的通知来自 "政务服务中心"(发布者)的通知: [通知] 恢复证件办理!可以看到订阅者有两个订阅者实例:jackLee(本人)、jackMa(可能是阿里出来的)共同订阅了证件办理信息。政务服务中心(发布者)第一次发布恢复通知的时候,jackLee 和 jackMa 这两个同学都订阅了消息,因此都受到了来自该中心的通知。后来,jackMa 可能由于已经派小弟火速前往该中心取走了他的证件不需要继续订阅了,于是该中心的工作人(发布者)员调用publisher.remove_subscriber(jackMa) 从该中心的订阅者记录表中移除了 jackMa 的订阅记录。于是,到了该中心第二次发布消息的时候,jackMa 已经不会再收到恢复证件办理消息,而 jackLee 还可以接收到恢复证件办理的消息。3. 通用型发布者对象的改进3.1 从 Subscriber 的服务员 到 事件的发布者在阅读本小节前请读者先自己尝试回答这个问题:Subscriber 类真的有必要实现吗?在我们上面的代码中,Subscriber 类 实现了几乎唯一一个有用的方法:update,它的作用却是给 Publisher 类的 notifyAll 方法进行调用。从现实生活中给一个解释:notifyAll 是消息的发布这发布消息的工具,update 是订阅用户接受到的新的定制化消息,比如同样是订阅了售房信息,但是由于不同类别的购房者订阅时所选定的楼层、大小等参数不一样,则这些不同订阅者接收到的发布结果不一样——也就表面原始的消息需要为不同的订阅者做一些 定制化 处理。在之前的代码实现中,这个消息的定制化工作就是使用 Subscriber 类的 update 方法实现的。很显然,上表面的代码要真正实现定制化,往往不仅是参数值的不同,可能对参数的处理也不一样。因此仅仅依赖参数data是不合理的。因此我们大概是需要写多个仅仅 update 方法的实现不同的 Subscriber 类——这不太好。略好一点的办法是,让 update 方法接受的不是单纯的数据 data,而是一个 回调函数 传入 update 方法中。先不着急修改我们的代码。对于发布者来说,似乎可以提供 更加周到 的服务——直接登记好订阅着的定制化需求处理方式,使用订阅者要求的处理方式处理好定制的消息后,直接告诉订阅者。——因此 update 这个接受表示用户定制化需求处理方式的方法可以直接合并到 发布者那边。于是 Subscriber 类 就不需要了,现在我们只需要更新一下我们的 Publisher。更新的思路是这样的:添加订阅者时(Publisher.addSubscriber)不仅需要记录订阅者名字,还要记录一个对应的响应函数用以消息发布后给订阅者提供定制化服务。从 Publisher 看,需要登记的内容又多了一些。不过好在 订阅者名称(认为是唯一标识符)和 与之对于的服务(回调函数),是对应的关系,既可以一对一,也可以一对多(表示这个订阅者需要多个定制化服务)。因此我们将 Publisher 的 “记录本”改成下面的类型:HashMap<String, Vec<EventCallback>>这个映射(Map)的 key 就表示 订阅者名称,而value 部分是一组函数,表示该订阅者需要的各种服务。另外,到了这里,对于 Publisher来说,添加订阅者就转化为了 为订阅者订阅各种定制化服务 。同时反过来看,对于某个具体的订阅者 Subscriber,一旦它的服务定制数组 (Function[])为空数组,表明他已经没有任何订阅,也不再需要接收发布者的任何消息了。因此先前我们使用的方法名addSubscriber 不适用了,从含以上换成 addListener 似乎更加合适。为什么呢? 我们接下来对此做进一步说明。一直以来,我们聚焦点都在于 发布者 和 订阅者,而忽略了 引起发布者发布的事件。 这个方法接受两个参数,一个是用户名,一个是为用户新增的回调。同时必须指出的是,这个 回调 往往是需要再其调用时接受一些数据的,比如由发布者发布的某些原始数据,他们就像是时时刻刻地 监听着、守候着 发布者 发布一个事件,一旦这个 事件/消息 被发布,就 完成消息发布后为 订阅者 所提供地服务。换一下思路,我们接着把聚焦点转移到 事件 上来。其实从现实中看,同一个事件发生,可能意味着可能需 要干很多事,既可以 服务更多的订阅者,也可以干其它任何的工作——我们一味地想着在发布者处登记订阅者的id然后完成订阅者的需求,那么 没有区别为何事件而需要去发布这些消息!更好的做法是 不再记录订阅者,而是记录为什么要发布消息给订阅者——也就是记录 事件。这样我们就可以在同一个事件发生的时候,通过一系列的属于该事件函数(可能一个或多个回调函数服务于同一个订阅者),完成该事件的响应,也就是回调函数们。从这个意义上看,我们所关注所谓的 订阅者 可以看作 一个事件发布后,发布者需要调用的一组函数。而所谓 发布,实际上就是调用这组函数以 完成事件(的回调)。因此我们接下来该用 listener 表示监听事件以待执行的回调函数, event 表示事件名, emit 表示这个事件发生后需要由发布者调用函数的过程。至此,我们的 Publisher 从一个 Subscriber 的服务员 转型成为了职业 事件的管理者,不妨给它改个名——EventEmitter。现在我们实现一个最基础的 EventEmitter 对象:use std::collections::HashMap;
type EventCallback = Box<dyn Fn()>;
struct EventEmitter {
/// 事件-监听器数组容器
_events: HashMap<String, Vec<EventCallback>>,
}
impl EventEmitter {
fn new() -> EventEmitter {
EventEmitter {
events: HashMap::new(),
}
}
/// 添加事件监听器,监听器是一个回调函数,表示用户订阅的具体服务
fn add_listener(&mut self, event: &str, callback: EventCallback) {
let callbacks = self._events.entry(event.to_string()).or_insert(Vec::new());
callbacks.push(callback);
}
/// 移除事件监听器:相当于用户取消订阅
fn remove_listener(&mut self, event: &str, callback: &EventCallback) {
if let Some(callbacks) = self._events.get_mut(event) {
callbacks.retain(|cb| cb != callback);
}
}
/// 触发事件:相当于发布消息或服务,也就是事件发生时,将订阅者订阅的服务一一为订阅者执行
fn emit(&self, event: &str) {
if let Some(callbacks) = self._events.get(event) {
for callback in callbacks {
callback();
}
}
}
}3.2 一个比较初步的功能增强不过很多时候我们还不满足于此,比如能够限制监听器的数量。从现实生活中打个比方,就像我们只服务一定数量的客户,一旦订满,由于资源有限,不再接收其它订阅。更贴切实际地说,就像节假日你去旅游景区地酒店订房间,对于酒店来说,一旦所有房间都预定满了,就不再接收新的订阅了——除非,有已经订阅地客人退订了它们先前已经预定地房间。实现这样一个功能,只需要一个变量 _max_listeners 作为最大监听器数量的控制变量。在外部相应的我们需要允许用户修改和读取该变量的值,因此还要提供 set_max_listeners 和 get_max_listeners 两个方法。use std::collections::HashMap;
use std::sync::{Arc, Mutex};
type EventCallback = Arc<dyn Fn() + Send + Sync>;
pub struct EventEmitter {
_events: Mutex<HashMap<String, Vec<EventCallback>>>,
_max_listeners: usize,
}
impl EventEmitter {
pub fn new() -> Self {
EventEmitter {
_events: Mutex::new(HashMap::new()),
_max_listeners: usize::MAX,
}
}
/// 设置最大监听器数量
/// Set the maximum number of listeners
pub fn set_max_listeners(&mut self, max_listeners: usize) {
self._max_listeners = max_listeners;
}
/// 获取最大监听器数量
pub fn get_max_listeners(&self) -> usize {
self._max_listeners
}
/// 添加事件监听器
pub fn add_listener(&self, event: &str, callback: EventCallback) {
let mut events = self._events.lock().unwrap();
let callbacks = events.entry(event.to_string()).or_insert(Vec::new());
callbacks.push(callback);
}
/// 移除事件监听器
pub fn remove_listener(&self, event: &str, callback: &EventCallback) {
let mut events = self._events.lock().unwrap();
if let Some(callbacks) = events.get_mut(event) {
callbacks.retain(|cb| !Arc::ptr_eq(cb, callback));
}
}
/// 触发事件
pub fn emit(&self, event: &str) {
let events = self._events.lock().unwrap();
if let Some(callbacks) = events.get(event) {
for callback in callbacks {
let callback_clone = callback.clone();
// Spawn a new thread to run each callback asynchronously
std::thread::spawn(move || {
(*callback_clone)();
});
}
}
}
}4. 使用现成的第三方模块:EventEmitter4.1 EventEmitter 的安装你可以直接使用 cargo 包管理器安装 EventEmitter:cargo add EventEmitter4.2 在你的项目中使用 EventEmitter以下是一段包括引入 EventEmitter 和使用的例子:use std::sync::{Arc};
use EventEmitter::EventEmitter;
fn main() {
let emitter = EventEmitter::new();
let callback1 = Arc::new(|| println!("[event1 emitted]: The first callback of event1 has been called."));
let callback2 = Arc::new(|| println!("[event1 emitted]: The second callback of event1 has been called."));
let callback3 = Arc::new(|| println!("[event2 emitted]: The only one callbask of event2 has been called."));
// Add event listener
emitter.on("event1", callback1);
emitter.on("event1", callback2);
emitter.on("event2", callback3);
let ct1 = emitter.listener_count("event1");
let ct2 = emitter.listener_count("event2");
println!("Number of Listeners for event1 is: {ct1}, \nNumber of Listeners for event2 is: {ct2}");
emitter.emit("event1"); // Emit event1
emitter.emit("event2"); // Emit event1
}运行项目:cargo run可以看到控制台打印结果:Number of Listeners for event1 is: 2,
Number of Listeners for event2 is: 14.3 EventEmitter 实例上的方法4.3.1 set_max_listeners 方法pub fn set_max_listeners(&mut self, max_listeners: usize)设置最大监听器数量。4.3.2 set_max_listeners 方法pub fn get_max_listeners(&self) -> usize获取最大监听器数量。4.3.3 on 方法pub fn on(&self, event: &str, callback: Arc<dyn Fn() + Send + Sync>)添加事件监听器。4.3.4 add_listener 方法pub fn add_listener(&self, event: &str, callback: Arc<dyn Fn() + Send + Sync>)添加事件监听器,是 on 方法的别名。4.3.5 off 方法pub fn off(&self, event: &str, callback: &Arc<dyn Fn() + Send + Sync>)移除事件监听器。4.3.6 remove_listener 方法pub fn remove_listener(
&self,
event: &str,
callback: &Arc<dyn Fn() + Send + Sync>
)移除事件监听器,是 off 方法的别名。4.3.7 emit 方法pub fn emit(&self, event: &str)触发事件。触发相当于“发布-订阅”模式中的“发布”,一但某个事件被触发,该事件对应得所有监听器函数都会被执行。监听器就相当于“订阅者”。4.3.8 remove_all_listeners 方法pub fn remove_all_listeners(&self, event: &str)移除所有事件的所有监听器。4.3.9 prepend_listener 方法pub fn prepend_listener(
&self,
event: &str,
callback: Arc<dyn Fn() + Send + Sync>
)从指定事件监听器向量的前方插入新的监听器。该方法与使用 on、add_listener 方法添加新的监听器时,插入监听器向量的方向相反。4.3.10 listeners 方法pub fn listeners(&self, event: &str) -> Vec<Arc<dyn Fn() + Send + Sync>>获取指定事件的监听器。4.3.11 listener_count 方法pub fn listener_count(&self, event: &str) -> usize获取指定事件的监听器数量。
Python 元编程
一个高级 Python 知识点是元编程(Metaprogramming),它指的是编写代码来操作程序本身(例如,动态修改代码、添加新的类或函数等)。元编程可以通过以下方法实现:
使用元类(Metaclasses):元类是用于创建类的类。在 Python 中,元类是派生自 type 的类,并定义了 __new__ 和 __init__ 两个方法。使用元类可以动态创建类、添加特定功能以及自定义类的行为。
使用装饰器(Decorators):装饰器是一种用于修改函数或类的行为的语法结构。装饰器可以用于创建缓存、跟踪执行时间、执行前后的处理等。装饰器也可以增加或删除函数的参数或者修改函数的返回值等。
动态创建类和函数:Python 是一种动态语言,可以动态创建类或函数。通过使用 type() 函数,可以在运行时创建一个新的类。通过使用 exec() 函数或者 eval() 函数,可以在运行时动态创建函数,这可以在构建 DSL 时非常有用。
使用协议(Protocols):协议指的是一套规范,用于描述对象应该具有的属性或者方法。在 Python 中,协议是一种非正式的接口,它定义了哪些方法或属性需要以特定方式实现。通过使用协议,可以实现多态(Polymorphism)或者混入(Mixin)的设计模式。
元编程在 Python 中被广泛使用,在许多框架和库中都有应用。例如,Django 中的 ORM 使用元类来生成 SQL 查询;Flask 中的装饰器用于定义路由;PyTorch 中的动态图机制使用了 Python 的动态创建类和函数等技术。