SegmentFault 最新的文章 |
- 中小型前端团队代码规范工程化最佳实践 - ESLint
- JDK/Dubbo/Spring 三种 SPI 机制,谁更好?
- 【2w字干货】ArrayList与LinkedList的区别以及JDK11中的底层实现
- 面试官一个线程池问题把我问懵逼了。
Posted: 12 Apr 2021 07:14 PM PDT 前言There are a thousand Hamlets in a thousand people's eyes. 一千个程序员,就有一千种代码风格。在前端开发中,有几个至今还在争论的代码风格差异:
这几个代码风格差异在协同开发中经常会被互相吐槽,甚至不能忍受。 除此之外,由于 JavaScript 的灵活性,往往一段代码能有多种写法,这时候也会导致协同时差异。并且,有一些写法可能会导致不易发现的 bug,或者这些写法的性能不好,开发时也应该避免。 为了解决这类静态代码问题,每个团队都需要一个统一的 JavaScript 代码规范,团队成员都遵守这份代码规范来编写代码。当然,靠人来保障代码规范是不可靠的,需要有对应的工具来保障,ESLint 就是这个工具。 有的读者看到这里,可能会说:Prettier 也可以保证代码风格一致。是的,Prettier 确实可以按照设置的规则对代码进行统一格式化,后面的文章也会有对应的介绍。但是需要明确的一点是,Prettier 只会在格式上对代码进行格式化,一些隐藏的代码质量问题 Prettier 是无法发现的,而 ESLint 可以。 关于 ESLint关于 ESLint,它的 Slogan 是 Find and fix problems in your JavaScript code。如上文所说,它可以发现并修复你 JavaScript 代码中的问题。来看一下官网上描述 ESLint 具备的三个特性:
基于以上描述,我们在前端工程化中可以这样使用 ESLint:
快速上手先简单介绍一下如何使用 ESLint,如果已经有所了解的同学,可以直接跳过这一节。 新建一个包含
安装
然后执行
生成好配置文件之后,就可以执行 简单配置我们来尝试配置 ESLint 的检查规则。以分号和引号举例,现在你作为团队代码规范的指定人,希望团队成员开发的代码,都是单引号和带分号的。 打开
然后我们将
执行
老老实实地按照规范修改代码,使用单引号并将加上分号。当然,如果你们希望是双引号和不带分号,修改相应的配置即可。 具体各个规则如何配置可以查看:https://eslint.org/docs/rules 自动修复执行 使用配置包在
这一行代码的意思是,使用 ESLint 的推荐配置。 因此,我们也可以使用任意封装好的配置,可以在 NPM 上或者 GItHub 上搜索
最佳实践简单了解完 ESLint 之后,对于 ESLint 的更多使用细节以及原理,在本篇文章就不展开了,感兴趣的朋友可以在官网详细了解。本文重点还是在于如何在团队工程化体系中落地 ESLint,这里提几个最佳实践。 抽象配置集对于独立开发者以及业务场景比较简单的小型团队而言,使用现成、完备的第三方配置集是非常高效的,可以较低成本低接入 ESLint 代码检查。 但是,对于中大型团队而言,在实际代码规范落地的过程中我们会发现,不可能存在一个能够完全符合团队风格的三方配置包,我们还是会在 这时候,就需要一个中心化的方式来管理配置包:根据团队代码风格整理(或者基于现有的三方配置集)发布一个配置集,团队统一使用这个包,就可以做到中心化管理和更新。 除此之外,从技术层面考虑,目前一个前端团队的面对的场景可能比较复杂。比如:
以上问题在真实开发中都是存在的,所以在代码规范的工程化方案落地时,一个单一功能的配置集是不够用的,这时候还需要考虑这个配置集如何抽象。 为了解决以上问题,这里提供一种解决方案的思路: 这里有一个 Demo,感兴趣的朋友可以看一下:eslint-config-axuebin 开发插件ESLint 提供了丰富的配置供开发者选择,但是在复杂的业务场景和特定的技术栈下,这些通用规则是不够用的。ESLint 通过插件的形式赋予了扩展性,开发者可以自定义任意的检查规则,比如 eslint-plugin-vue / eslint-plugin-react 就是 Vue / React 框架中使用的扩展插件,官网也提供了相关文档引导开发者开发一个插件。 一般来说,我们也不需要开发插件,但我们至少需要了解有这么个东西。在做一些团队代码质量检查的时候,我们可能会有一些特殊的业务逻辑,这时候 ESLint 插件是可以帮助我们做一些事情。 这里就不展开了,主要就是一些 AST 的用法,照着官方文档就可以上手,或者可以参考现有的一些插件写法。 脚手架 / CLI 工具当有了团队的统一 ESLint 配置集和插件之后,我们会将它们集成到脚手架中,方便新项目集成和开箱即用。但是对于一些老项目,如果需要手动改造还是会有一些麻烦的,这时候就可以借助于 CLI 来完成一键升级。 本文结合上文的 Demo eslint-config-axuebin,设计一个简单的 CLI Demo。由于当前配置也比较简单,所以 CLI 只需要做几件简单的事情即可:
核心代码如下:
可运行的 CLI Demo 代码见:axb-lint,在项目目录下执行: 自动化配置了 ESLint 之后,我们需要让开发者感知到 ESLint 的约束。开发者可以自己运行 eslint 命令来跑代码检查,这不够高效,所以我们需要一些自动化手段来做这个事情。当然 在开发时,编辑器也有提供相应的功能可以根据当前工作区下的 ESLint 配置文件来检查当前正在编辑的文件,这个不是我们关心的重点。 一般我们会在有以下几种方式做 ESLint 检查:
这里提一下 pre-commit 的方案,在每一次本地开发完成提交代码前就做 ESLint 检查,保证云端的代码是统一规范的。 这种方式非常简单,只需要在项目中依赖 husky 和 lint-staged 即可完成。安装好依赖之后,在 package.json 文件加入以下配置即可:
效果如图所示: 总结本文介绍了 ESLint 在中小型前端团队的一些最佳实践的想法,大家可以在此基础上扩展,制订一套完善的 ESLint 工作流,落地到自己团队中。 本文是前端代码规范系列文章的其中一篇,后续还有关于 StyleLint/CommitLint/Prettier 等的文章,并且还有一篇完整的关于前端代码规范工程化实践的文章,敬请期待( 更多原创文章欢迎关注公众号「玩相机的程序员」,或者加我微信 xb9207 交流 | ||||||||||||||||||||||||
JDK/Dubbo/Spring 三种 SPI 机制,谁更好? Posted: 12 Apr 2021 05:50 PM PDT 先点赞再看,养成好习惯 SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。 本文主要是特性 & 用法介绍,不涉及源码解析(源码都很简单,相信你一定一看就懂) SPI 有什么用?举个栗子,现在我们设计了一款全新的日志框架:super-logger。默认以XML文件作为我们这款日志的配置文件,并设计了一个配置文件解析的接口:
然后来一个默认的XML实现:
那么我们在初始化,解析配置时,只需要调用这个XMLConfiguration来解析XML配置文件即可。
这样就完成了一个基础的模型,看起来也没什么问题。不过扩展性不太好,因为如果想定制/扩展/重写解析功能的话,我还得重新定义入口的代码,LoggerFactory 也得重写,不够灵活,侵入性太强了。 比如现在用户/使用方想增加一个 yml 文件的方式,作为日志配置文件,那么只需要新建一个YAMLConfiguration,实现 SuperLoggerConfiguration 就可以。但是……怎么注入呢,怎么让 LoggerFactory中使用新建的这个 YAMLConfiguration ?难不成连 LoggerFactory 也重写了? 如果借助SPI机制的话,这个事情就很简单了,可以很方便的完成这个入口的扩展功能。 下面就先来看看,利用JDK 的 SPI 机制怎么解决上面的扩展性问题。 JDK SPIJDK 中 提供了一个 SPI 的功能,核心类是 java.util.ServiceLoader。其作用就是,可以通过类名获取在"META-INF/services/"下的多个配置实现文件。 为了解决上面的扩展问题,现在我们在
然后通过 ServiceLoader 获取我们的 SPI 机制配置的实现类:
最后在调整LoggerFactory中初始化配置的方式为现在的SPI方式:
等等,这里为什么是用 iterator ? 而不是get之类的只获取一个实例的方法? 试想一下,如果是一个固定的get方法,那么get到的是一个固定的实例,SPI 还有什么意义呢? SPI 的目的,就是增强扩展性。将固定的配置提取出来,通过 SPI 机制来配置。那既然如此,一般都会有一个默认的配置,然后通过 SPI 的文件配置不同的实现,这样就会存在一个接口多个实现的问题。要是找到多个实现的话,用哪个实现作为最后的实例呢? 所以这里使用iterator来获取所有的实现类配置。刚才已经在我们这个 super-logger 包里增加了默认的SuperLoggerConfiguration 实现。 为了支持 YAML 配置,现在在使用方/用户的代码里,增加一个YAMLConfiguration的 SPI 配置:
此时通过iterator方法,就会获取到默认的XMLConfiguration和我们扩展的这个YAMLConfiguration两个配置实现类了。 在上面那段加载的代码里,我们遍历iterator,遍历到最后,我们**使用最后一个实现配置作为最终的实例。 再等等?最后一个?怎么算最后一个? 使用方/用户自定义的的这个 YAMLConfiguration 一定是最后一个吗? 这个真的不一定,取决于我们运行时的 ClassPath 配置,在前面加载的jar自然在前,最后的jar里的自然当然也在后面。所以如果用户的包在ClassPath中的顺序比super-logger的包更靠后,才会处于最后一个位置;如果用户的包位置在前,那么所谓的最后一个仍然是默认的XMLConfiguration。 举个栗子,如果我们程序的启动脚本为:
默认的XMLConfiguration SPI配置在 但这个classpath顺序如果反了呢?main.jar 在前,super-logger.jar 在后
这样一来,iterator 获取的最后一个元素又变成了默认的XMLConfiguration,我们使用 JDK SPI 没啥意义了,获取的又是第一个,还是默认的XMLConfiguration。 由于这个加载顺序(classpath)是由用户指定的,所以无论我们加载第一个还是最后一个,都有可能会导致加载不到用户自定义的那个配置。 所以这也是JDK SPI机制的一个劣势,无法确认具体加载哪一个实现,也无法加载某个指定的实现,仅靠ClassPath的顺序是一个非常不严谨的方式 Dubbo SPIDubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,我们先来了解一下 Java SPI 与 Dubbo SPI 的用法,然后再来分析 Dubbo SPI 的源码。 Dubbo 中实现了一套新的 SPI 机制,功能更强大,也更复杂一些。相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下(以下demo来自dubbo官方文档)。
与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外在使用时还需要在接口上标注 @SPI 注解。下面来演示 Dubbo SPI 的用法:
Dubbo SPI 和 JDK SPI 最大的区别就在于支持"别名",可以通过某个扩展点的别名来获取固定的扩展点。就像上面的例子中,我可以获取 Robot 多个 SPI 实现中别名为"optimusPrime"的实现,也可以获取别名为"bumblebee"的实现,这个功能非常有用! 通过 @SPI 注解的 value 属性,还可以默认一个"别名"的实现。比如在Dubbo 中,默认的是Dubbo 私有协议:dubbo protocol - dubbo://
在 Protocol 接口上,增加了一个 @SPI 注解,而注解的 value 值为 Dubbo ,通过 SPI 获取实现时就会获取 Protocol SPI 配置中别名为dubbo的那个实现,
然后只需要通过getDefaultExtension,就可以获取到 @SPI 注解上value对应的那个扩展实现了
还有一个 Adaptive 的机制,虽然非常灵活,但……用法并不是很"优雅",这里就不介绍了 Dubbo 的 SPI 中还有一个"加载优先级",优先加载内置(internal)的,然后加载外部的(external),按优先级顺序加载,如果遇到重复就跳过不会加载了。 所以如果想靠classpath加载顺序去覆盖内置的扩展,也是个不太理智的做法,原因同上 - 加载顺序不严谨 Spring SPISpring 的 SPI 配置文件是一个固定的文件 -
下面是一段 Spring Boot 中 spring.factories 的配置
Spring SPI 中,将所有的配置放到一个固定的文件中,省去了配置一大堆文件的麻烦。至于多个接口的扩展配置,是用一个文件好,还是每个单独一个文件好这个,这个问题就见仁见智了(个人喜欢 Spring 这种,干净利落)。 Spring的SPI 虽然属于spring-framework(core),但是目前主要用在spring boot中…… 和前面两种 SPI 机制一样,Spring 也是支持 ClassPath 中存在多个 spring.factories 文件的,加载时会按照 classpath 的顺序依次加载这些 spring.factories 文件,添加到一个 ArrayList 中。由于没有别名,所以也没有去重的概念,有多少就添加多少。 但由于 Spring 的 SPI 主要用在 Spring Boot 中,而 Spring Boot 中的 ClassLoader 会优先加载项目中的文件,而不是依赖包中的文件。所以如果在你的项目中定义个spring.factories文件,那么你项目中的文件会被第一个加载,得到的Factories中,项目中spring.factories里配置的那个实现类也会排在第一个 如果我们要扩展某个接口的话,只需要在你的项目(spring boot)里新建一个
对比
三种 SPI 机制对比之下,JDK 内置的机制是最弱鸡的,但是由于是 JDK 内置,所以还是有一定应用场景,毕竟不用额外的依赖;Dubbo 的功能最丰富,但机制有点复杂了,而且只能配合 Dubbo 使用,不能完全算是一个独立的模块;Spring 的功能和JDK的相差无几,最大的区别是所有扩展点写在一个 spring.factories 文件中,也算是一个改进,并且 IDEA 完美支持语法提示。 各位看官们大佬们,你们觉得 JDK/Dubbo/Spring 三种 SPI 的机制,哪个更好呢?欢迎评论区留言 参考
原创不易,未经授权禁止转载。如果我的文章对您有帮助,请点赞/收藏/关注鼓励支持一下吧❤❤❤❤❤❤ | ||||||||||||||||||||||||
【2w字干货】ArrayList与LinkedList的区别以及JDK11中的底层实现 Posted: 12 Apr 2021 01:10 AM PDT 1 概述本文主要讲述了 2 两者区别在详细介绍两者的底层实现之前,先来简单看一下两者的异同。 2.1 相同点
2.2 不同点
3 | ||||||||||||||||||||||||
Posted: 12 Apr 2021 09:31 PM PDT 这是why的第 98 篇原创文章 前几天,有个朋友在微信上找我。他问:why哥,在吗? 我说:发生肾么事了? 他啪的一下就提了一个问题啊,很快。 我大意了,随意瞅了一眼,这题不是很简单吗? 结果没想到里面还隐藏着一篇文章。 故事,得从这个问题说起: 上面的图中的线程池配置是这样的:
上面这个线程池里面的参数、执行流程啥的我就不再解释了。 毕竟我曾经在《一人血书,想让why哥讲一下这道面试题。》这篇文章里面发过毒誓的,再说就是小王吧了: 上面的这个问题其实就是一个非常简单的八股文问题: 非核心线程在什么时候被回收? 如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,就会被回收。 标准答案,完全没毛病。 那么我现在带入一个简单的场景,为了简单直观,我们把线程池相关的参数调整一下:
那么问题来了:
上面这三个问题的答案都是肯定的,如果你搞不明白为什么,那么我建议你先赶紧去补充一下线程池相关的知识点,下面的内容你强行看下去肯定是一脸懵逼的。 接下来的问题是这样的:
先说答案:还是 3 个。 从我个人正常的思维,是这样的:核心线程是空闲的,每隔 3 秒扔一个耗时 1 秒的任务过来,所以仅需要一个核心线程就完全处理的过来。 那么,30 秒内,超过核心线程的那一个线程一直处于等待状态,所以 30 秒之后,就被回收了。 30 秒之后,超过核心线程的线程并不会被回收,活跃线程还是 3 个。 可以不看,拉到最后,点个赞,去忙自己的事情吧。 如果你不知道,可以接着看,了解一下为什么是 3 个。 虽然我相信没有面试官会问这样的问题,但是对于你去理解线程池,是有帮助的。 先上 Demo基于我前面说的这个场景,码出代码如下:
这份代码也是提问的哥们给我的,我做了微调,你直接粘出去就能跑起来。 show me code,no bb。这才是相互探讨的正确姿势。 这个程序的运行结果是这样的: 一共五个任务,线程池的运行情况是什么样的呢? 先看标号为 ① 的地方: 三个线程都在执行任务,然后 2 号线程和 1 号线程率先完成了任务,接着把队列里面的两个任务拿出来执行(标号为 ② 的地方)。 按照程序,接下来,每隔 3 秒就有一个耗时 1 秒的任务过来。而此时线程池里面的三个活跃线程都是空闲状态。 那么问题就来了: 该选择哪个线程来执行这个任务呢?是随机选一个吗? 虽然接下来的程序还没有执行,但是基于前面的截图,我现在就可以告诉你,接下来的任务,线程执行顺序为:
即虽然线程都是空闲的,但是当任务来的时候不是随机调用的,而是轮询。 由于是轮询,每三秒执行一次,所以非核心线程的空闲时间最多也就是 9 秒,不会超过 30 秒,所以一直不会被回收。 基于这个 Demo,我们就从表象上回答了,为什么活跃线程数一直为 3。 为什么是轮询?我们通过 Demo 验证了上面场景中,线程执行顺序为轮询。 那么为什么呢? 这只是通过日志得出的表象呀,内部原理呢?对应的代码呢? 这一小节带大家看一下到底是怎么回事。 首先我看到这个表象的时候我就猜测:这三个线程肯定是在某个地方被某个队列存起来了,基于此,才能实现轮询调用。 所以,我一直在找这个队列,一直没有找到对应的代码,我还有点着急了。想着不会是在操作系统层面控制的吧? 后来我冷静下来,觉得不太可能。于是电光火石之间,我想到了,要不先 Dump 一下线程,看看它们都在干啥: Dump 之后,这玩意我眼熟啊,AQS 的等待队列啊。 根据堆栈信息,我们可以定位到这里的源码:
看到这里的时候,我才一下恍然大悟了起来。 害,是自己想的太多了。 说穿了,这其实就是个生产者-消费者的问题啊。 三个线程就是三个消费者,现在没有任务需要处理,它们就等着生产者生产任务,然后通知它们准备消费。 由于本文只是带着你去找答案在源码的什么地方,不对源码进行解读。 所以我默认你是对 AQS 是有一定的了解的。 可以看到 addConditionWaiter 方法其实就是在操作我们要找的那个队列。学名叫做等待队列。 Debug 一下,看看队列里面的情况: 巧了嘛,这不是。顺序刚好是:
消费者这边我们大概摸清楚了,接着去看看生产者。
线程池是在这里把任务放到队列里面去的。 而这个方法里面的源码是这样的: 其中 这个方法里面会调用 而唤醒的顺序,就是等待队列里面的顺序: 所以,现在你知道当一个任务来了之后,这个任务该由线程池里面的哪个线程执行,这个不是随机的,也不是随便来的。 是讲究一个顺序的。 什么顺序呢? Condition 里面的等待队列里面的顺序。 什么,你不太懂 Condition? 那还不赶紧去学?等着我给你讲呢? 本来我是想写一下的,后来发现《Java并发编程的艺术》一书中的 5.6.2 小节已经写的挺清楚了,图文并茂。这部分内容其实也是面试的时候的高频考点,所以自己去看看就好了。 先欠着,欠着。 非核心线程怎么回收?还是上面的例子,假设非核心线程就空闲了超过 30 秒,那么它是怎么被回收的呢? 这个也是一个比较热门的面试题。 这题没有什么高深的地方,答案就藏在源码的这个地方:
当 timed 参数为 true 的时候,会执行 而 timed 什么时候为 true 呢?
allowCoreThreadTimeOut 默认为 false。 所以,就是看 因此 timed 为 true。 也就是说,当前 workQueue 为空的时候,现在三个线程都阻塞 workQueue.poll 方法中。 而当指定时间后,workQueue 还是为空,则返回为 null。 于是在 1077 行把 timeOut 修改为 true。 进入一下次循环,返回 null。 最终会执行到这个方法:
而这个方法里面会执行 remove 的操作。 于是线程就被回收了。 所以当超过指定时间后,线程会被回收。 那么被回收的这个线程是核心线程还是非核心线程呢? 不知道。 因为在线程池里面,核心线程和非核心线程仅仅是一个概念而已,其实拿着一个线程,我们并不能知道它是核心线程还是非核心线程。 这个地方就是一个证明,因为当工作线程多余核心线程数之后,所有的线程都在 poll,也就是说所有的线程都有可能被回收: 另外一个强有力的证明就是 addWorker 这里: core 参数仅仅是控制取 corePoolSize 还是 maximumPoolSize。 所以,这个问题你说怎么回答: JDK 区分的方式就是不区分。 那么我们可以知道吗? 可以,比如通过观察日志,前面的案例中,我就知道这两个是核心线程,因为它们最先创建:
在程序里面怎么知道呢? 目前是不知道的,但是这个需求,加钱就可以实现。 自己扩展一下线程池嘛,给线程池里面的线程打个标还不是一件很简单的事情吗? 只是你想想,你区分这玩意干啥,有没有可落地的需求? 毕竟,脱离需求谈实现。都是耍流氓。 最后说一句才疏学浅,难免会有纰漏,如果你发现了错误的地方,可以在后台提出来,我对其加以修改。 感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。 |
You are subscribed to email updates from SegmentFault 最新的文章. To stop receiving these emails, you may unsubscribe now. | Email delivery powered by Google |
Google, 1600 Amphitheatre Parkway, Mountain View, CA 94043, United States |
No comments:
Post a Comment