在实际项目中,经常需要用到邮件通知功能。比如,用户通过邮件注册,通过邮件找回密码等;又比如通过邮件发送系统情况,通过邮件发送报表信息等等,实际应用场景很多。
正常我们会用 JavaMail 相关 api 来写发送邮件的相关代码,但现在 SpringBoot 提供了一套更简易使用的封装。这篇文章,阿粉就带大家通过 SpringBoot 快速的实现发送邮件的功能。
Java 's Blog
在实际项目中,经常需要用到邮件通知功能。比如,用户通过邮件注册,通过邮件找回密码等;又比如通过邮件发送系统情况,通过邮件发送报表信息等等,实际应用场景很多。
正常我们会用 JavaMail 相关 api 来写发送邮件的相关代码,但现在 SpringBoot 提供了一套更简易使用的封装。这篇文章,阿粉就带大家通过 SpringBoot 快速的实现发送邮件的功能。
Hello 大家好,我是阿粉,作为团队中的主程阿粉经常参与很多核心功能的开发,而且很多时候一个需求没做好中间又插入新的紧急的需求或者 bug 修复,每次遇到这种情况,如果两个地方代码不冲突的话还好,可以直接在本分支修改然后提交,但是当遇到需要修改同一个类文件的时候就比较麻烦了。这种情况如何优雅的处理呢?让阿粉来带你了解 Git 的高级骚操作!
之前的时候,阿粉一直在看同事面试,但是呢,阿粉并没有自己去面试,而无意间打开Boss的时候,发现一家公司私聊了我,我回复了一下之后,竟然说我可以去面试,不曾想,面试一个问题,让我的薪资瞬间被砍掉了5K,你如果不想自己出去要的薪资被砍,那么你要会设计这个。 <–more–>
像阿粉的朋友所在的公司属于一个中小型的公司,公司的项目都是按照客户的要求来定制进行开发的,而服务器的数量那是少的可怜,什么高并发,不考虑,什么高可用,也不考虑,一台服务器上面部署了自己的项目,有时候连个图片服务器都没有,他们的图就是这个样子的。
是不是看着很简陋的样子,直接浏览器客户端和单一服务器之间进行交流了,如果访问人数在前人以上,比如秒杀个限量款,那估计可能直接就凉了,“Boom”的一下就访问失败了。
这个时候一般网站架构还是采用单体架构,但是服务器也相对的增加了,终于增加了数据库服务器和应用类服务器在加上图片服务器,算是组成了一个小小的进阶版本的项目架构。
也就是说我们在部署应用的时候,手动把应用服务器上的Tomcat给关掉,然后替换系统的代码war包,接着重新启动Tomcat。
这时候把数据单独的部署在另外的一个服务器上面,存放网站的全部核心数据。
然后在另外一台独立的服务器上部署NFS作为图片服务器,存放网站的全部图片。
这时候呢,代码执行的是请求,数据访问在另外一台服务器,而图片在另外一台服务器上面,这样就通过增加了服务器来部署的普通版本的项目架构就完成了,而大部分的项目都是这个样子的吧,毕竟小公司人力有限呀。
不知道大家在面试的时候说这个的时候面试官有没有问过,如果你们的应用服务器挂了,你们怎么处理?
是的,应用服务器真的会出事,那在项目的应用服务器真的出事之后,我们怎么处理,在换个服务器,那么你很多想对应的配置就要做出更改,需要更改一大堆的东西,那么你会有很长时间的维护期,维护代价很高了呀。
这时候我们就出现了这个:
如果说我们有一台其中的应用服务器宕机了,不干活了,那么接下来,另外的一台服务器就会正常的使用,并且,在同时使用的时候,我们还能够把从浏览器来的大量的请求分发一下,直接对半劈成2份,如果说其中有一个服务器的配置“高的飞起”,那么我们还可以通过配置,给他的请求让他多一点,“设置权重”,这样让他分担更多的压力,而那个比较 low 的服务器的话,承担的相应的请求就会减少一点,毕竟性能比较低。
而在这个时候还会出现另外一种意外,那就是数据库服务器宕机了,怎么办?相同的原理,增加另外的一个数据库服务器,也就是主从数据库。
那么为什么要做主从复制,不单单是因为这一个服务器宕机的问题,还有如果对数据库的读和写都在同一个数据库服务器中操作,业务系统性能会降低。
而我们很多的项目在数据上面需要的就是比较重要的,就很多就是做读写分离,而为了提升业务系统性能,优化用户体验,可以通过做主从复制(读写分离)来减轻主数据库的负载。
一台用于写入数据,一台用于同步主的数据并用于数据查询操作。
配置好主从复制之后,同一张表,只能对一个服务器写操作。
现在很多项目的架构都是所有人的业务代码都写在了一起,乱七八糟的,好几个人的代码,维护起来那叫一个崩溃,当你看到这样的项目的时候,你就会发现,人都傻了,这是个什么鬼,改动一点,其他的不好用了,那叫一个崩溃呀。
这时候我们就需要把我们的业务彻底的做出分割,比如商城里面的,订单属于一块的业务,积分属于一部分的业务,物流属于另外一部分的业务,这时候我们就需要分成三块的业务模块。
也就是相当于每一块的内容都属于一个服务,而这些服务叠加起来可就不是1+1=2的问题了,这些业务服务相加起来,这时候完整的项目和你把所有的内容同一写到一个部分的内容差距是非常大的,尤其是在代码冗余方面,做的那是相当透彻的。
而说到这给内容的话,其实已经算差不多了,但是再更深的阿粉是真的没有了解到那么多,而阿粉之前没想过这些内容会在你面试的时候问到,可能阿粉当时只是想自己做个咸鱼,不去关心架构方面的事情,而架构方面的事情确实很多公司很看重的一点。
在这里阿粉给大家再次贡献出此次面试的其他的问题,并且给出详细的解答,并且阿粉也是很欣慰的,只是压了工资,而没有说直接拒绝。
客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。大多数客户端已经实现了客户端分区。
代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。
查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。
这些都是阿粉死记硬背背下来的,关于这种东西,没有实际亲身实践过的,没有遇到过的问题的,都是属于被diss的,但是面试官好像理解也不是很深,也是知道,但是具体的也没有仔细的深挖我这块的内容。
说实话,这个问题,面试好像现在都是必问的知识点了,阿粉的同事面试也是被问,但是巧了,之前阿粉就写过关于SpringBean的生命周期的,文章链接给大家送上,大家可以仔细查看,可以共同交流呦。
面试官:兄弟你来阐述一下Spring框架中Bean的生命周期?
所以大家一定要把这个准备好呦,SpringBean的生命周期,从初始化到最终的销毁中间经历了什么,过程是什么,流程怎么理解。
这个问题我在回答的时候我就分开了,分门别类的比较比如说:
线程安全
性能优劣
NULL
实现方式
容量扩容
我分成了五块的内容作答,如果说你只能够刚刚想起来其中的三到四点也是不错的,至少比你只会说他们的线程安全和是否允许键为空来的好很多,如果大家有兴趣,请寻找集合系列文章,在过往的历史当中寻找一下,有很多关于HashMap 和 Hashtable的面试点。
这个我相信只要是工作了两到三年的程序员肯定都会,我就给大家简单说说,之前的文章图解,
关于两篇文章通俗易懂,简单明了,几次面试,回答没有遗漏,面试官觉得还行,文章连接送上:
关于其他的问题,都不是比较经典的,都是属于项目业务中的了,阿粉就不再一一给大家介绍了,总结起来就一句话,基础你要掌握,扩展你更要会,不然面试想要高工资?那是不可能的,毕竟不是每家公司都缺“大爷”,不是么?
阿粉在这里希望大家找到自己理想的工作,等疫情稳定了,直接跳槽工资“Double”。
现在的面试和几年前的面试差距很大了,现在培训机构出来的同学很多都占一大部分,而且那张口要的可是真的有点多,甚至比一些工作两三年的朋友们还多,不排除有一些大牛的存在,但是还是有一些不是很给力的,但是想一口吃撑的,而有经验的面试官,分分钟就能看出来你到底是有工作经验还是没有工作经验的。而分布式中的内容,将会是极其重要的一点内容。
Hello 大家好,我是阿粉,Markdown 语法想必很多写作的朋友都了解(不了解的可以网上找找,绝对是写作装逼必备神器),Typora 软件必定是 Markdown 写作的一大利器(阿粉不接受反驳),今天阿粉给大家分享一下 Typora 的几个骚操作,我们来一起看下吧。
全是阿粉想要推荐给你的
Hello 大家好,我是阿粉,工作这么多年虽然经历过风风雨雨,但是每次线上发布版本的时候都是一场硬战,这不最近发布了一个版本,一不小心写了个 bug,差点造成了生产事故,幸好运维老大发现及时。
是阿粉用了心写的搭建教程
本文总结了Java集合容器的经典面试题,所有题目阿粉都给出了自己思考,适合面试前复习扫盲使用。阿粉不能保证里面包含了所有集合面试题,但只要认真深挖好每一道题,做到触类旁通,就能以不变应万变。
能把上面的基本框架答出来基本就没问题了,对于各种类型我只列举了一些实际工作中常用的实现类。但其实在Set,List和Queue下还有更细的划分,如果想要在面试时表现一番,那得对着JDK好好背一背了>_<
fail-fast是一种错误检测机制,Java在适合单线程使用的集合容器中很好地实现了fail-fast机制,举一个简单的例子:在多线程并发环境下,A线程在通过迭代器遍历一个ArrayList集合,B线程同时对该集合进行增删元素操作,这个时候线程A就会抛出并发修改异常,中断正常执行的逻辑。
而fail-safe机制更像是一种对fail-fast机制的补充,它被广泛地实现在各种并发容器集合中。回头看上面的例子,如果线程A遍历的不是一个ArrayList,而是一个CopyOnWriteArrayList,则符合fail-safe机制,线程B可以同时对该集合的元素进行增删操作,线程A不会抛出任何异常。
要理解这两种机制的表象,我们得了解这两种机制背后的实现原理:
我们同样用ArrayList解释fail-fast背后的原理:首先ArrayList自身会维护一个modCount变量,每当进行增删元素等操作时,modCount变量都会进行自增。当使用迭代器遍历ArrayList时,迭代器会新维护一个初始值等于modCount的expectedModCount变量,每次获取下一个元素的时候都会去检查expectModCount和modCount是否相等。在上面举的例子中,由于B线程增删元素会导致modCount自增,当A线程遍历元素时就会发现两个变量不等,从而抛出异常。
CopyOnWriteArrayList所实现的fail-safe在上述情况下没有抛出异常,它的原理是:当使用迭代器遍历集合时,会基于原数组拷贝出一个新的数组(ArrayList的底层是数组),后续的遍历行为在新数组上进行。所以线程B同时进行增删操作不会影响到线程A的遍历行为。
这种题目我觉得要先答出核心原理,如果你对多线程和单线程下容器的使用有自己的见解,可以考虑多聊点。
使用集合迭代器自身的remove方法进行删除
1 |
|
可能笔试考的更多,算是Java的基本常识吧
本质的区别来源于两者的底层实现:ArrayList的底层是数组,LinkedList的底层是双向链表。
数组拥有O(1)的查询效率,可以通过下标直接定位元素;链表在查询元素的时候只能通过遍历的方式查询,效率比数组低。
数组增删元素的效率比较低,通常要伴随拷贝数组的操作;链表增删元素的效率很高,只需要调整对应位置的指针即可。
以上是数组和链表的通俗对比,在日常的使用中,两者都能很好地在自己的适用场景发挥作用。
比如说我们常常用ArrayList代替数组,因为封装了许多易用的api,而且它内部实现了自动扩容机制,由于它内部维护了一个当前容量的指针size,直接往ArrayList中添加元素的时间复杂度是O(1)的,使用非常方便。
而LinkedList常常被用作Queue队列的实现类,由于底层是双向链表,能够轻松地提供先入先出的操作。
我觉得可以分两部分答,一个是数组与链表底层实现的不同,另一个是答ArrayList和LinkedList的实现细节。
两者的底层实现相似,关键的不同在于Vector的对外提供操作的方法都是用synchronized修饰的,也就是说Vector在并发环境下是线程安全的,而ArrayList在并发环境下可能会出现线程安全问题。
由于Vector的方法都是同步方法,执行起来会在同步上消耗一定的性能,所以在单线程环境下,Vector的性能是不如ArrayList的
除了线程安全这点本质区别外,还有一个实现上的小细节区别:ArrayList每次扩容的大小为原来的1.5倍;Vector可以指定扩容的大小,默认是原来大小的两倍。
感觉可以顺带谈谈多线程环境下ArrayList的替代品,比如CopyOnWriteArrayList,但是要谈谈优缺点。
由于ArrayList有自动扩容机制,所以ArrayList的elementData数组大小往往比现有的元素数量大,如果不加transient直接序列化的话会把数组中空余的位置也序列化了,浪费不少的空间。
ArrayList中重写了序列化和反序列化对应的writeObject和readObject方法,在遍历数组元素时,以size作为结束标志,只序列化ArrayList中已经存在的元素。
细节题
HashMap死亡连环Call即将来临,看爽了记得点个赞啊
先热身
JDK1.8
首先计算key的hash值,计算过程是:先得到key的hashCode(int类型,4字节),然后把hashCode的高16位与低16位进行异或,得到key的hash值。
接下来用key的hash值与数组长度减一的值进行按位与操作,得到key在数组中对应的下标。
计算key在数组中的下标时,是通过hash值与数组长度减一的值进行按位与操作的。由于数组的长度通常不会超过2^16,所以hash值的高16位通常参与不了这个按位与操作。
为了让hashCode的高16位能够参与到按位与操作中,所以把hashCode的高16位与低16位进行异或操作,使得高16位的影响能够均匀稀释到低16位中,使得计算key位置的操作能够充分散列均匀。
在极端情况下,比如说key的hashCode()返回的值不合理,或者多个密钥共享一个hashCode,很有可能会在同一个数组位置产生严重的哈希冲突。
这种情况下,如果我们仍然使用使用链表把多个冲突的元素串起来,这些元素的查询效率就会从O(1)下降为O(N)。为了能够在这种极端情况下仍保证较为高效的查询效率,HashMap选择把链表转换为红黑树,红黑树是一种常用的平衡二叉搜索树,添加,删除,查找元素等操作的时间复杂度均为O(logN)
至于阈值为什么是8,这是HashMap的作者根据概率论的知识得到的。当key的哈希码分布均匀时,数组同一个位置上的元素数量是成泊松分布的,同一个位置上出现8个元素的概率已经接近千分之一了,这侧面说明如果链表的长度达到了8,key的hashCode()肯定是出了大问题,这个时候需要红黑树来保证性能,所以选择8作为阈值。
如果是7的话,那么链表和红黑树之间的切换范围值就太小了。如果我的链表长度不停地在7和8之间切换,那岂不是得来回变换形态?所以选择6是一种折中的考虑。
在JDK1.7中,迁移数据的时候所有元素都重新计算了hash,并根据新的hash重新计算数组中的位置。
在JDK1.8中,这个过程进行了优化:如果当前节点是单独节点(后面没有接着链表),则根据该节点的hash值与新容量减一的值按位与得到新的地址。
如果当前节点后面带有链表,则根据每个节点的hash值与旧数组容量进行按位与的结果进行划分。如果得到的值为0,这些元素会被分配回原来的位置;如果得到的结果不为0,则分配到新位置,新位置的下标为当前位置下标加上旧数组容量。
还有一种情况是当前节点是树节点,那么会调用一个专门的拆分方法进行拆分。
开放性题目?以下是个人见解:
如果要支持动态缩容,可能就要把缩容安排在remove方法里,这样可能会导致remove方法的时间复杂度从O(1)上升为O(N)。
还有一点可能和我们编写Java代码的习惯有关:由于Java有自动垃圾回收机制,让我们得以可劲地new对象,Java也默认了我们这种吃饭不收拾盘子的行为。既然对象会被回收,HashMap动态缩容在这样的大环境下似乎就显得没那么重要了,这可以说是一种空间换时间的策略吧。
因为这些基础类内部已经重写了hashCode和equals方法,遵守了HashMap内部的规范。
一定要重写hashCode()和equals()方法,而且要遵从以下规则:
equals()是我们判断两个对象是否相同的依据,如果我们重写了equals方法,用自己的逻辑去判断两个对象是否相同,那么一定要保证:
两个equals()返回true的对象,一定要返回相同的hashCode。
这样,在HashMap的put方法中才能正确判断key是否相同。
不是经常有一个问题嘛,两个对象hashCode相同,equals一定返回true吗?答案肯定是否的,这和你的设计密切相关:如果在你的编程思路中这两个对象是不同的,那么就算恰巧两个对象的hashCode相同,equals也应该返回false。
因为这样能够提高根据key计算数组位置的效率。
HashMap根据key计算数组位置的算法是:用key的hash值与数组长度减1的值进行按位与操作。
在我们正常人的思维中,获取数组的某个位置最直接的方法是对数组的长度取余数。但是如果被除数是2的幂次方,那么这个对数组长度取余的方法就等价于对数组长度减一的值进行按位与操作。
在计算机中,位运算的效率远高于取模运算,所以为了提高效率,把数组的长度设为2的幂次方。
在JDK1.7之前,两者的实现极为相似,最大的区别在于HashTable的方法都用synchronized关键字修饰起来了,表明它是线程安全的。
但是由于直接在方法上加synchronized关键字的同步效率较低,在并发情况下,官方推荐我们使用ConcurrentHashMap。
所以我们看到在JDK1.8中,官方甚至没有对HashTable进行链表转树这样的优化,HashTable已经不被推荐使用了。
在JDK1.7中ConcurrentHashMap采用了一种分段锁的机制,它的底层实现是一个segment数组,每个segment的底层结构和HashMap相似,也是数组加链表。
当对segment里面的元素进行操作之前,需要获得该segment独有的一把ReentrantLock。ConcurrentHashMap如果不进行手动设置的话,默认有16个segment,可以支持16个线程对16个不同的segment进行并发写操作。
在JDK1.8之后摒弃了segment这种臃肿的设计,新的实现和HashMap非常相似,底层用的也是数组加链表加红黑树。
在新实现中,在put方法里使用了CAS + synchronized进行同步。如果插入元素的位置为空,则使用CAS进行插入。如果插入的位置不为空,则对当前位置的对象进行加锁,也就链表或红黑树的头节点,加锁后再进行后续的插入操作。
这样设计的好处是:
这道题如果想深挖扩展可以开始往Java多线程并发方面扯:synchronized,CAS。Java多线程方面我也会出一份总结,有兴趣的不妨先点赞关注一波
我感觉面试的时候对集合的考察会偏向实现原理多一些,所以一定要看一遍源码,相比于框架的源码,集合的源码简直太友好了。在笔试的时候可能还会考一些集合的使用,比如遍历,排序,比较等等,这些算是Java基础了,用得多也就熟了。
最后如果你觉得阿粉的回答有问题,欢迎指正!
不夸张的说,遇到的问题, 80% 都可以通过浏览器搜索解决,但是呢,有时候你会发现,我搜索的内容挺对的呀,为什么我就找不到想要的内容,别人就可以找到呢
Hello 大家好,我是阿粉,最近心情不是很好,因为阿粉面试阿里三面挂掉了, 当收到下面这封邮件的时候阿粉内心是拔凉拔凉的。阿粉被 “Unfortunately”,“another candidate” 这几个词深深的伤害到了。不过伤心归伤心,该自我总结还是得自我总结的,有机会再战。
阿粉万字长文带你解析 ThreadPoolExecutor
最近疫情稳定了,跳槽的换工作这块,又开始了,这不,阿粉公司的同事因为公司不涨工资的事情,已经开始踏上了重新面试,寻找更高级别的出路了,而前几天的面试,让他一次大好的机会被京东的线程面试题阻挡在了门外。
对于一个开发工程师来说,了解一下 MySQL 是如何执行一条查询语句的,不是一件坏事,阿粉带你来瞅瞅它是怎么执行的
菜单权限管理,一直都是后端管理系统必不可少的一个模块,今天我们就一起来瞅瞅,如何精准的控制到按钮。
阿粉最近碰到一个场景,用户注册之后需要发送邮件给其邮箱。原先设计中,这是一个同步过程,注册方法需要等待邮件发送成功才能返回。
由于邮件发送流程对于注册来说并不是一个关键节点,我们可以将邮件发送异步执行,减少注册方法执行时间。
我们可以自己创建线程池,然后执行异步任务,示例代码如下:
hello~各位读者好,我是鸭血粉丝(大家可以称呼我为「阿粉」)。今天,阿粉带着大家来了解一下获取
mapper.xml
配置文件中的sql
我们都知道,把首页数据放到Redis里,能够加快首页数据的访问速度。但是我们要如何准确又快速的将 Redis 整合到自己的 SpringBoot2.x 项目中呢?今天阿粉就带大家爬一爬其中的门门道道。
我们都知道,新建一个对象的时候实现 Serializeable
接口,但为什么要这么做?什么时候这样子做?这样子做会不会出现幺蛾子?阿粉一个三连差点把自己都问懵逼了……