Loading... ## 技术面试指导 ### 一、必备项 #### 0.自我介绍 表达流畅、不要太差即可。可以提前准备一份说辞,外企需要准备英文的自我介绍。 #### 1.基础 **`坑1:【答案很标准】`** 面试时的回答,一定不要背网上的《面试大全》/《面试宝典》,一定要有自己的思想。这些答案太过千篇一律、标准。而且太老的面试题可能存在错误或者过时。 **建议:** 每道题都通过自己的理解或者代码敲一遍,能够用自己话说出来,最好加上自己的一些理解(哪怕有点瑕疵)。或者搬运一些博客/GitHub上大牛的博文。不要死记那些所谓的标准答案(那些答案往往很浅)。 总之,就是要向面试官传达“我的答案是我自己写的,我是一个有独立思考的人,而不是网上抄的”。 #### 2.技术列表 **`坑2:【精通】`** 3年以内的开发者,几乎没人敢说“精通”哪一门技术 **建议:** 掌握程度:掌握、熟练、理解、会使用 **`坑3:【个人掌握的技能过于“标准化”,明显就是培训、或者看某套视频学出来。如:java + 数据库 + web前端 + jsp/servlet + ssm + boot/cloud】`** **建议:** 一般而言,自学成才的人比培训出来的学生 更具有独立思考的能力,因此在相同的条件下,企业更喜欢没有参加过培训的学生。 建议写上2-3门非培训机构标配课程,如service mesh、netty等(最好写与高并发、分布式有关的,技术的名字相对“少见”但又很重要的)。对面试而言,这些“少见”的技术,只要你写上了,并且能把其中任意一个核心知识点说明白,就已经非常加分了。(假设Spring是一个“少见”的技术,那么你只要在面试时解释一下什么是IoC就可以了) **`坑4:【简历上写一大堆牛B的技术,显得自己很厉害】`** **建议:** 技术点宁可少写,也别多写。面试官经常都很忙,没时间精心准备对你的面试,甚至有时候是一边神游一边在提问,所以很可能从你简历里随便挑几个你写上的技术来问你。因此简历上写到的技术,都很可能被问到。 所以写上去的所有技术都要事先做好面试官提问的准备,对于非主流的技术,面试官一般会让你自己介绍使用场景和方法,可以事先做好准备。 #### 3.项目 **`坑4:【项目名叫“xxx电商项目”、“xxx管理系统”,这些“项目”简直就是培训机构的标配,缺乏真实项目的感觉】`** 但对于实习生来说,这个坑真的是痛点。因为除此和课设的“xxx管理系统”之外,很难接手到其他新的项目。大创之类的也不一定刚好是所学语言为主。 **`建议:`** 0. 针对实习生来说,可以学一学Java爬虫,做完后开源出来。 1. 提前准备好回答“项目”的剧本 “你做过什么样的项目?”或者根据你简历中的项目来提问,几乎是技术面试官必须做、并且非常喜欢做的事。所以,如果你没有充足的项目经验,就提前准备好台词吧。(用到的技术、使用场景、遇到的困难、如何解决) 2. 关于项目,经常会被问到的点是:某个技术本身的不足,以及如何弥补。因为这样问,能够检验你是否真的做过这个“项目”,至少能说明你是否深入思考过。举例如下: * 你项目中用到了MySQL :如果数据超过的MySQL的容量怎么处理?(弥补MySQL自身的不足) * 你做的这个项目是高并发吧?缓存用了吗?在哪些场景 你见过缓存失效?怎么解决?(还是在问你缓存自身的问题如何解决) * 看你的项目用到了MQ?MQ可以用来解耦合,具体讲讲你项目中到底哪些场景用到了解耦合?(在考你的项目是真的,还是假的) 3. 项目的重难点 每个项目都有自己的重难点,这些重难点也就是必问点,举例如下 * 分布式项目:如何共享数据?什么是CAP原则?分布式锁、分布式事务、分布式缓存怎么实现? * 高并发项目:几级缓存,如何限流,如何熔断,用docker了没? 4. 真实性:实际的使用场景 * 简历上写的“用到了人脸识别技术” :哪些场景用到了?人脸识别是自己公司写的,还是调用的三方API?自己写的话,用的什么算法?调用API的话,每次调用需要付费多少钱?识别时的光线强度有什么要求? * 多线程、设计模式、算法:用来处理什么业务?场景? * 大数据的项目:数据从哪来的? * 项目能否访问? 5. 描述方式:技术列表 + 文字 (如果绘图功底不错,可以加上架构图) 6. 项目周期:半年以上。在校生的话就写2、3个月的项目周期。 简历上的项目个数:三个及以内(毕业三年内,写1-2个就够了) #### 4.表达沟通能力 不用刻意表现,表达清楚就行。问到不会的不甩锅,不抱怨,诚恳听取认真改正,体现自己的素养。 ### 二、加分项 #### 1.高并发/分布式/调优 1. 多线程(juc、aqs、线程安全、锁机制、生产消费者、线程依赖问题) 2. 数据处理SQL优化 , 常见高性能数据库架构(如mysql+mycat+haproxy+keepalived) 3. JVM调优 #### 2.实际的解决问题能力 这点需要自己在面试时主动将话题引入。例如在回答项目时,主动说一下你在做项目时遇到过什么问题。具体是如何发现、排查、分析、解决问题的。 #### 3.绝杀 * ACM竞赛、蓝桥杯等全国性竞赛(学生专享) * 有过书籍、论文等出版物在github发布过项目(star很多) * 博客、微信公众号、 个人在阿里云等部署的可访问项目(这一条大部分人都能做到)。如果是电子简历,附上链接地址;如果是纸质简历,将链接封装在二维码里。 * 研究过JDK/spring/mybatis等源码 ### 三、注意事项 1. 在描述时,多使用具体“数字”(前提是比模糊的更能体现自己)几个项目、几篇博客、排名第几、几次奖学金、几个证书 2. 工资:不要写面议 ,至少给个薪资范围,如1.5w - 2.0w 工资写面议HR可能没办法预算,比较赶的时候就直接pass掉了。而且一般会给工资最低范围多个一点点,如果自我感觉技术还行,可以适当提点薪资下限。 3. 简历:1-2页写满,不要放空,不要包书皮,电子版用PDF。简洁大方即可,不用太过绚丽。 4. 细节:毕业时间、年龄、工作履历、期望薪资等要相互匹配。例如,不要“毕业5年”,但“工作履历加起来只有3年”。 5. 沟通:注意人文素养 ,不要抱怨问题,要体现解决问题、愿意承担责任的态度 建议:体现出个人解决问题的能力、团队感、沟通能力,也可以在简历中表明。 --- ### 四、出奇制胜 * **反对和所有 应聘者 千篇一律** 建议:积累《阿里巴巴编程规范》(IDEA加插件)、《effective java》 * **反对和所有《面试宝典》千篇一律** 先用自己的答案写,在看看博客上别人的总结和心得,再结合宝典中的标准答案。 * **源码级解决问题** 当学习中产生一个问题时,是如何解决的?百度、谷歌、问别人。 但是面试官喜欢认识到我们有独立思考的一面,所以我们要尽可能的告知是通过自己查询书籍、阅读源码得出的。比如问到字符串相加时,点明 `+`号的底层方法是调用 `StringBuffer/StringBuilder`的 `append()`方法;如下例子: > **请讲讲ArrayList如何库容的?** > 我最初看书(看博客、文档),了解到它的底层是动态数组,在add()时如果发现已满,则自动扩容1.5 。再通过查阅源码验证,发现是在数组已满时扩容,并且是通过位移运算符扩容。 > `int newCapacity = oldCapacity + (oldCapacity >> 1);` * **找准时机,秀技能** 每个人都有一些独到的经验,要想办法在面试的时候讲给面试官。<br>“聊聊自己的项目经验” ,讲项目,讲完的时候加一句:我在做这个项目的时候曾经遇到了一个bug,当时ArrayList.asList()返回一个List,但是执行add()方法时报错了。当时通过阅读源码发现返回的List不是Collection中的那个List而是asList()方法返回后内置的List,它不包括add()方法。 * **比较通用的秀点** **1.优化类(JVM、SQL优化)** JVM优化:项目做完时,我用jmeter进行了压力测试,结果发现相应时间太慢(或者内存利用率太高) ,然后我用jvisualvm分析了下JVM的内存情况。分析后,进一步发现项目中的小对象太多了,并且发现这些小对象都是生命周期比较短的对象。然后我猜测可能就是由于短对象太多,造成了堆中新生代容量不足,进而让很多短对象逃逸到了老年代中。这样一来,新生代和老年代中的对象都会很多,就会加速GC的回收频率,从而降低系统的性能。对此,我调大了新生代的内存大小,并且调高了新生代 逃逸到老年代的阈值。之后再测试,发现性能平稳了许多。 (PS:以上短短200字,就告诉了面试官你有性能测试的良好习惯、会发现问题、分析问题,并且会JVM调优!) SQL优化:项目做完时,我用jmeter进行了压力测试,结果发现响应时间太慢。然后我用mysqldumslow工具查找到了项目中执行时间最长的那个SQL语句,因此猜测是这条SQL的性能太低,拖累了整个系统。然后我用explain查看了SQL执行计划,发现这个SQL根本没有写索引并且是大表驱动了小表,所以特别慢。之后,我给它后面的where查询字段加上了索引,并且改为了小表驱动大表。然后再次测试,响应时间就缩短了很多。 (PS:这个秀点是“SQL优化”,具体流程是:定位慢SQL->使用explain查询SQL执行计划,用于分析SQL执行慢的原因->SQL优化。上述中的“小表驱动大表”等是SQL优化时的术语。) **2.算法类(KMP算法优化字符串匹配等)** * **技术沉淀** 数据支撑:GitHub、个人博客、专栏、微信公众号、项目发布到阿里云 * **大厂区别** bat、美团、阿里、字节调动、google、facebook等大厂的看重点: 数据结构和算法、操作系统、网络、设计模式、分布式、逻辑思维 * **心态建设** 回答正确,但被面试官“否定”: 1.答案来自于面试宝典,千篇一律 2.面试官自己在秀技术,调整好心态,不要受干扰。 正常面试官:引导你来回答,而不是想方设法吓唬你 问:有没有用过List? 哪些常见子类?数组?动态扩容,不越界? 3.如果问到自己不会的?一定要答出自己的想法/答类似的框架: 虽然没用过这个新框架,但是我用过同类产品,并且相信他们是差不多的,然后就答这个同类产品的实现原理。 如:对于mybatis/jpa,若出现新dao框架? 则表明相信他们的工作原理都是映射文件/注释:实体类-表,然后通过框架本身的api的进行crud....等等。 --- ## 五、SSM和开源框架(部分)面试考点梳理 ### Mybatis * 了解Mybatis的相关组件和配置规约 * **Mybatis开发时的常用对象** 1.SqlSessionFactory:SqlSesssion工厂。通过SqlSessionFactory:SqlSesssion中的openSession()产生SqlSesssion对象。 2.SqlSesssion:SqlSesssion对象(类似于JDBC中的Connection) 3.Executor:MyBatis中所有Mapper语句的执行 都是通过Executor进行的。 * **Mybatis四大核心对象** 1.StatementHandler(负责sql语句):数据库的处理对象 select... from where id = #{} .. 2.PrameterHandler(负责sql中的参数):处理SQL中的参数对象3.Executor 4.ResultSetHandler:处理SQL的返回结果集 * **Mybatis四大处理器** StatementHandler、PrameterHandler、ResultSetHandler、TypeHandler (类型转换器) * **执行流程** ```Java //(1)获取配置文件信息 Reader reader = Resources.getResourceAsReader("conf.xml") ; //(2)创建SqlSessionFactory SqlSessionFactory sessionFacotry = new SqlSessionFactoryBuilder().build(reader,"development") ; //(3)通过SqlSessionFactory创建SqlSession SqlSession session = sessionFacotry.openSession() ; //(4)通过SqlSession执行数据库操作(获取XxxMapper再执行相应sql语句) StudentMapper studentMapper = session.getMapper(StudentMapper.class) ; Student student = studentMapper.queryStudentByStuno(1) ; //(5)调用session.commit()提交事务 session.commit(); //(6)session.close()关闭会话 session.close(); ``` * **一对一、一对多、多级缓存等。见之前博客** ### Spring **IOC/DI:控制反转/依赖注入** 含义:IOC帮我们提供了一个工厂。先向工厂中注入对象(配置[xml、注解]),再从工厂中获取对象。IOC可以让我们通过“配置”的方式来创建对象。 目的:解耦合 问题:Student2 student = new Student2()使用new会造成耦合度较高 解决:用工厂模式,使 `类->new->对象`到 `类->工厂模式->对象`,但需要手动编写(引申到手撕工厂模式) **AOP:面向切面编程(OOP的补充而不是代替)** 提问:OOP的不足、AOP对此的改进之处、AOP的使用场景有哪些:日志和安全统一校验等、如何实现 **Spring用到了哪些设计模式** 工厂模式:创建bean、获取bean 单例模式/原型模式:创建bean时,设置作用域。(顺便手写singleton/prototype) 监听模式:自定义时间发布,监听模式。如ApplicationListener,当某个动作触发时,就会自动执行一个通知。 ### SpringMVC ### SpringBoot **自动装配原理:**约定优于配置 (核心:将一些配置功能,前置到源码底层实现好) **自动装配两个特点:** 1.版本仲裁中心:因此,以后引入依赖时,不用再写版本号。好处:1.不用记 2.避免冲突(防止引入多个引来时,由于各个依赖的版本不兼容造成的冲突) 2.提供了很多starter(场景启动器) :批量jar。 假设开发web项目需要用到 ` json.jar tomcat.jar hibernate-validator.jar, spring-web.jar ...`则把他们统一封装到 `spring-boot-starter-web`启动场景。 以后使用web项目,只需要引入spring-boot-starter-web。 **自动装配的应用时:** @EnableAutoConfiguration就是springboot提供自动装配的注解。 **SpringBoot启动流程** ### 后续还有SpringCloud、Eureka、Ribbon、Feign、熔断器以及各种分布式架构等 --- ## 代码部分 > **1.字符串匹配问题,判断是否相等** ```Java import java.util.Objects; /** * @author 唐宋丶 * @create 2020/9/15 * * 出奇制胜:判断字符是否相等 */ public class Demo01 { public static void main(String[] args) { String str = "abc"; //大多数人写法 System.out.println(str.equals("abc")); //'"abc"' 应该作为方法 "equals()"的调用方,而不是参数 ,需要翻转来避免空指针问题 System.out.println("abc".equals(str)); //Objects工具类提供equals比较方法 System.out.println(Objects.equals("abc",str)); } } ``` Objects.equals()源码如下: ```Java /** * Returns {@code true} if the arguments are equal to each other * and {@code false} otherwise. * Consequently, if both arguments are {@code null}, {@code true} * is returned and if exactly one argument is {@code null}, {@code * false} is returned. Otherwise, equality is determined by using * the {@link Object#equals equals} method of the first * argument. * * @param a an object * @param b an object to be compared with {@code a} for equality * @return {@code true} if the arguments are equal to each other * and {@code false} otherwise * @see Object#equals(Object) */ public static boolean equals(Object a, Object b) { return (a == b) || (a != null && a.equals(b)); } ``` > **2.线程通信:生产者—消费者问题** > 一般采用wait()和notify()/notifyAll() > 出奇制胜可以采用Semaphore、Google的guava类库的Monitor ```Java import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.Monitor; import com.google.common.util.concurrent.MoreExecutors; import java.util.LinkedList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; /** * @author 唐宋丶 * * 出奇制胜:通过google guava类库的Monitor完成生产-消费者问题的线程通信 */ public class MonitorDemo { private LinkedList<Integer> buffer = new LinkedList<>(); private static final int MAX = 10; //记录生产的数据编号 private static AtomicInteger count = new AtomicInteger(0); //int i ; i++ ;非原子性操作 ; i++ 拆分成多行 ;线程安全问题 private Monitor monitor = new Monitor(); /** * 生产数据,返回值是void,传入是int */ public void produce(int value) { try { //enterWhen:相当于加锁 monitor.enterWhen(monitor.newGuard(() -> buffer.size() < MAX)); buffer.addLast(value); } catch (InterruptedException e) { e.printStackTrace(); } finally { monitor.leave(); System.out.println("生产完毕,缓冲区大小:" + buffer.size()); } } /** * 消费数据,传入是void,返回值是int */ public int consume() { try { monitor.enterWhen(monitor.newGuard(() -> !buffer.isEmpty())); return buffer.removeFirst(); } catch (InterruptedException e) { e.printStackTrace(); throw new RuntimeException(e); } finally { monitor.leave(); System.out.println("消费完毕,缓冲区大小:" + buffer.size()); } } public static void main(String[] args) { MonitorDemo demo = new MonitorDemo(); ExecutorService executorService = Executors.newFixedThreadPool(6); //将线程池包装 ListeningExecutorService listeningExecutorService = MoreExecutors.listeningDecorator(executorService); //向线程池放入3个生产者 for (int i = 0; i < 3; i++) { listeningExecutorService.submit(() -> { while (true) { int result = count.getAndIncrement(); //i++ 0 1 2 demo.produce(result); System.out.println("生产:" + result); } }); } //向线程池放入3个消费者 for (int i = 0; i < 3; i++) { while (true) { int result = demo.consume(); System.out.println("消费" + result); } } } } ``` > **3.如何设计一个秒杀系统?** 秒杀含义:流量很大,库存少(10000用户抢购5件商品) 从以下**架构设计原则**回答:**限流、缓存、隔离、降级和熔断** * **限流(多重)** **尽量上级限流**:在用户看得到的页面限流,如:加验证码防止非法机器人的刷新/攻击、JavaScript限制IP请求次数、隐藏秒杀入口地址,只有用户正常进入商品页面才显示秒杀按钮,不能直接输入秒杀按钮的链接 **负载均衡**:在网络分层第四层通过搭载lvs集群分发请求到Nignx集群、在网络分层第七层搭载Nginx分流到Tomcat集群中+动静分离:动态请求(需要通过java代码查询数据返回结果)发送到后台服务器;静态请求(图片、视频、大且数据唯一的文件)发送到CDN **MQ流量削峰**:在请求和服务器之间加个MQ队列,只存放服务器可承受的请求数。 * **缓存(多级)**: **Redis缓存**:Java服务器请求->redis缓存->DB数据库。 **DB处理**:搭建高可用架构:VIP虚拟IP技术(`keepalived+haproxy`将IP分为多个节点,某个节点挂掉后能自动切换到可用节点)+MyCat读写分离(秒杀系统一般读多写少)+读/写数据库MySQL主从同步 * **隔离**: 将秒杀的服务器隔离开,确保即使秒杀挂掉了其他操作还能正常执行 * **降级和熔断**: **熔断**:客户端请求时调用service()超时->`备用方法(){return "未响应,请重试"}` **降级**:服务端 :20个服务->减少到10个服务(内部节约资源),通过牺牲非必要服务来提高性能。 * **防止重复消费行为:幂等性** 支付->支付服务(扣款)->返回(支付成功)过程中若因网络等问题支付成功提示失败有可能导致用户重复付款。 幂等性实现:去重表,在每次支付前,先查看去重表中 是否有扣款记录。如果有,则返回文字提示“已扣款,请稍等响应页面”;如果没有,再执行扣款 > **4.简述Nignx** **Nginx是一款轻量级的web服务器(反向代理服务器,负载均衡服务器,动静分离服务器)。** **(1)反向代理** 正向代理(VPN):代理客户端去访问服务器。如: `(用户→VPN)→google` 反向代理(Nginx/Apache):代理服务器接收服务端的请求(处理后再转发给服务器)。如: `客户端→(Nginx→Tomcat)` **(2)负载均衡** 分流客户端请求,均衡集群服务端压力。 Nignx支持 `weight轮询(默认)`、`ip_hash`、`fair`、`url_hash`四种负载均衡调度算法。 **(3)动静分离** 分离静态请求和动态请求,将动态请求发送给Web服务器(TomCat),并给静态请求做缓存(或CDN加速)。 **Nginx优点** 1. 高并发连接: 官方测试Nginx能够支撑5万并发连接,实际生产环境中可以支撑2~4万并发连接数。 2. Nginx为开源软件,成本低廉。 3. 稳定性高:用于反向代理时,基本无宕机,非常稳定。 4. 支持热部署/热维护:能够在不间断服务的情况下,进行Nignx扩增和维护。 > **5.用多种方法实现“多线程(三线程)交替打印123123”** 方法一: ```Java package DT.thread_test; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author 唐宋丶 * @create 2020/9/16 * * 建立三个线程,第一个线程打印1、第二个线程打印2、第三个线程打印3;要求三个线程交替打印,123123123123…… * * 通过锁方式一: lock.lock()/unlock(): await() signal() */ public class LoopPrint { private static int num = 1; /** * 一把读写锁,锁定变量的更改 */ Lock lock = new ReentrantLock(); /** * 三个线程通知 线程打印 123123123... * 线程1判断num是否为1,不是等待,是打印并通知线程2 * 线程2判断num是否为2,不是等待,是打印并通知线程3 * 线程3判断num是否为3,不是等待,是打印并通知线程1 */ Condition condition1 = lock.newCondition(); Condition condition2 = lock.newCondition(); Condition condition3 = lock.newCondition(); public static void main(String[] args) { LoopPrint loopPrint = new LoopPrint(); //打印1的线程 new Thread(() -> { while (true){ loopPrint.print1(); } }).start(); new Thread(() -> { while (true){ loopPrint.print2(); } }).start(); new Thread(() -> { while (true){ loopPrint.print3(); } }).start(); } public void print1(){ lock.lock(); try{ if(num != 1){ condition1.await(); } System.out.println(1); Thread.sleep(1000); num = 2; condition2.signal(); }catch (Exception e){ System.out.println(e); }finally { lock.unlock(); } } public void print2(){ lock.lock(); try{ if(num != 2){ condition2.await(); } System.out.println(2); Thread.sleep(1000); num = 3; condition3.signal(); }catch (Exception e){ System.out.println(e); }finally { lock.unlock(); } } public void print3(){ lock.lock(); try{ if(num != 3){ condition3.await(); } System.out.println(3); Thread.sleep(1000); num = 1; condition1.signal(); }catch (Exception e){ System.out.println(e); }finally { lock.unlock(); } } } ``` 方法二: ```Java package DT.thread_test; /** * @author 唐宋丶 * @create 2020/9/16 * * * 建立三个线程,第一个线程打印1、第二个线程打印2、第三个线程打印3;要求三个线程交替打印,123123123123…… * * * * 通过锁方式二: Synchronized wait()、notify()/notifyAll() */ public class LoopPrint2 { private static int num = 1; Thread t1 = new Thread(() ->{ while (true){ print1(); } }); Thread t2 = new Thread(() ->{ while (true){ print2(); } }); Thread t3 = new Thread(() ->{ while (true){ print3(); } }); public static void main(String[] args) throws InterruptedException { LoopPrint2 loopPrint2 = new LoopPrint2(); loopPrint2.t1.start(); loopPrint2.t2.start(); loopPrint2.t3.start(); } public void print1(){ //保证当前线程是在t3执行完施放锁以后才执行的 synchronized (t3){ synchronized (this){ if(num == 1){ System.out.println(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } num = 2; this.notifyAll(); } } t3.notifyAll(); } } public void print2(){ synchronized (t1){ synchronized (this){ if(num == 2){ System.out.println(2); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } num = 3; this.notifyAll(); } } t1.notifyAll(); } } public void print3(){ synchronized (t2){ synchronized (this){ if(num == 3){ System.out.println(3); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } num = 1; this.notifyAll(); } } t2.notifyAll(); } } } ``` 方法三: ```Java package DT.thread_test; import java.util.concurrent.Semaphore; /** * @author 唐宋丶 * @create 2020/9/16 * * 建立三个线程,第一个线程打印1、第二个线程打印2、第三个线程打印3;要求三个线程交替打印,123123123123…… * * 通过信号量争夺方式实现交替打印 */ public class SemaphorePrint { /** * 三个信号量争夺一个许可证 */ Semaphore sem1 = new Semaphore(1); Semaphore sem2 = new Semaphore(0); Semaphore sem3 = new Semaphore(0); public static void main(String[] args) { SemaphorePrint semaphorePrint = new SemaphorePrint(); new Thread(() -> semaphorePrint.print1()).start(); new Thread(() -> semaphorePrint.print2()).start(); new Thread(() -> semaphorePrint.print3()).start(); } public void print(String value,Semaphore current, Semaphore next){ while (true){ try { //当前信号量获取许可证 current.acquire(); System.out.println(Thread.currentThread().getName() + ": " + value); Thread.sleep(1000); /** * 在实现中不包含真正的许可对象,并且Semaphore也不会将许可与线程关联起来, * 因此在一个线程中获得的许可可以在另一个线程中释放。可以将acquire操作视为是消费一个许可,而release操作是创建一个许可,Semaphore并不受限于它在创建时的初始许可数量。 * * print1中,next.release释放一个许可,此时已经选中print2的线程,所以下一个获取到许可的信号量必定是线程2,以此类推 */ next.release(); } catch (InterruptedException e) { e.printStackTrace(); } } } public void print1(){ print("1",sem1,sem2); } public void print2(){ print("2",sem2,sem3); } public void print3(){ print("3",sem3,sem1); } } ``` > **6.一个类的声明能否 既是abstract,又是final?** > 不能。 > abstract修饰的类为抽象类,不能被实例化,只能通过子类extend继承它;而final修饰的类不能被继承。所以两者在能否被“继承”方面是矛盾语义,不能同时使用。 > **7.如何给final修饰成员变量的初始化赋值?** * final:用于声明属性,方法和类,分别表示属性不可变(对象属性的引用类型不可变),方法不可覆盖,类不可继承。内部类要访问局部变量,局部变量必须定义成final类型。final修饰的属性没有默认值。 此题分为两种情况: 情况一、没有static:可以直接通过 `=`赋值;也可以通过构造方法给final赋值,但要注意对于每个构造方法都必须给final变量赋值。 ```Java public class FinalDemo01 { //final修饰的变量,没有默认值 //final int num = 10; final int num; public FinalDemo(){ this.num = 10 ; } public FinalDemo(int num ){ //只要存在final修饰的属性的构造方法都必须给final属性赋值 this.num = num ; } //构造方法的作用? 实例化对象new(任意一个构造方法都可以实现) public static void main(String[] args) { FinalDemo demo = new FinalDemo(); System.out.println(demo.num); } } ``` 情况二、有static:只能通过 `=`初始化值,而不能用构造方法赋值。因为static变量是在构造方法之前执行的。 **补充:** (1)`static`修饰变量的执行时机:类加载过程中执行,并且会被初始化为默认值,若有赋初值则在后续被赋予。 ```Java class A { //a -> 0 ->10 static int a = 10; } ``` (2)`final`修饰的变量执行时机:运行时被初始化(直接赋值,也可以通过构造方法赋值) (3)`static final`修饰的变量执行时机:在javac时(编译)生成ConstantValue属性,在类加载的过程中根据ConstantValue属性值为该字段赋值。ConstantValue属性值没有默认值,必须显示的通过=赋值。 > **8.为什么对于一个public及final修饰的变量,一般建议声明为static?** > 如果没有static修饰:假设在10个类中都要使用类A中的num变量,就必须在这10个类中先实例化A的对象,然后通过 `对象.num`来使用num变量,因此至少需要new 10次。 > 加上static可以节约内存。static是共享变量,因此只会在内存中拥有该变量的一份存储,之后就可以直接通过 `类名.变量`使用(例如 `A.num`),而不用先new后使用。 > 由于final修饰的变量不可改变,因此不用考虑并发问题。 > **9.InterfaceA是接口,`InterfaceA []a = new InterfaceA[2];`是否正确?** > 正确。 > 在对象数组a[]中实际存放的不是对象本身,而是对象的引用地址。此题a[]中为 `null`。 ```Java class InterfaceA { ... } class ClassA implements InterfaceA { } Public class Test { public static void main(String[] args){ InterfaceA []a = new InterfaceA[2]; //new生成的对象在堆内存中,a[]中存放的是对象引用的地址 a[0] = new ClassA(); a[1] = new ClassA(); } } ``` > **9.==和equals()的区别?** > (在面试大全中已经讲解过,这边简单的再过一遍) > `==`为比较运算符,比较的是基本数据类型的值和对象的引用地址(内存地址) > `equlas()`最初是在Object中定义的一个方法。在Object中定义的 `equals()`就是 `==`。只不过一般来说其子类都会重写 `equlas()`方法,将其重写为比较“内容”是否相等。如:String。 > **10.举例说明如何重写equals()方法** > 假定Person对象的name和age相同,则返回true;否则返回false。 ```Java public class Person { private String name; private int age ; public Person() { } public Person(String name,int age) { this.age = age; this.name = name; } ... //约定:如果name和age相同,则返回true @Override public boolean equals(Object obj) { if (this == obj) { return true; } //name age if(obj instanceof Person){ Person per = (Person)obj ; //用传入的per,和当前对象this比较 if(this.name.equals(per.getName()) && this.age==per.getAge()){ return true ; } } return false; } //约定:如果重写equals()则要重写hashCode(),防止map等方法导致结果与约定的equals结论不一致 @Override public int hashCode() { return this.name.hashCode() & age; } } ``` > **11.重写euqals()为什么要重写hashCode()方法** > 用上题例子来说明: ```Java public class EqualsDemo { public static void main(String[] args){ Person p1 = new PerSon('zs',23); Person p2 = new PerSon('zs',23); //Map中的key根据对象的hashcode存储 Map<Person,String> map = new HashMap<>(); map.put(p1,'AAA'); map.put(P2,'BBB'); //AAA 根据上题的equals两个p1p2相等,因该为同一个对象。所以两个人的hashcode应该也是一样的,实际上map中应该只能存放一个p(p1=p2),最终值为BBB覆盖AAA System.out.println(map.get(p1)); //BBB System.out.println(map.get(p2)); } } ``` **总结:** 两个对象相等(引用地址),则hashcode一定相同 如果两个对象的hashcod相同,这俩对象不一定相同。 两个对象不等(引用地址),hashcode不一定不等 如果两个对象的hashcod不同,这俩对象一定不同。 **判断元素内容是否相等(Map/Set等):** (1)根据hashCode快速定位(提高效率,避免了在大量集合中由于遍历带来的效率问题) (2)若hashCode相等,根据equals判断内容是否相同 (判断正确性,避免偶然) 所以,如果要判断元素内容是否相同,就要重写hashCode()和equals()。 > **12.TCP三次握手和四次挥手** > 原因:任何数据传输都无法保证绝对的可靠性 > 初始化状态:客户端处于close关闭状态,服务器处于Listen监听状态。 **三次握手:SYN和ACK** 建立连接(双向确认),完成三次握手,客户端与服务器开始传送数据。 过程: (1)第一次握手:客户端发送请求报文将 SYN=1 同步序列号和初始化 seq=x发送给服务端,服务端从初始化状态,创建连接,等待客户端,确认接收后的状态为SYN_Receive。这个时候客户端处于等待状态为SYN_Send。 (2)第二次握手,服务器接收到报文后(SYN=1,seq=x)收到请求后请求报文变为同步序列号SYN=1,初始化序列号seq=1,确认号ACK=1,ack=x+1,服务器为SYN_Receive状态,发送端的状态为:SYN_Send。 (3)第三次握手,客户端收到服务端的数据包(收到响应后),然后发送同步序列号ack=y+1和数据包的序列号seq=x+1和ACK=1确认包作为应答(第三次握手:ACK=1,seq=x+1,ack=y+1),客户端和服务端变化为established状态。 **四次挥手:数据传送完毕,完成四次挥手关闭连接** (1)第一次挥手:客户端设置seq和 ACK ,向服务器发送一个 FIN=1报文段。此时,(第一次挥手,FIN=1,seq=u)客户端进入 FIN_WAIT 状态,表示客户端没有数据要发送给服务端了。 (2)第二次挥手:服务端收到了客户端发送的 FIN 报文段,向客户端回了一个 ACK 报文段。 (3)第三次挥手:服务端向客户端发送FIN 报文段,请求关闭连接,同时服务端进入 LAST_ACK 状态。 (4)第四次挥手:客户端收到服务端发送的 FIN 报文段后,向服务端发送 ACK 报文段,然后客户端进入 TIME_WAIT 状态。服务端收到客户端的 ACK 报文段以后,就关闭连接。此时,客户端等待 2MSL(指一个片段在网络中最大的存活时间)后依然没有收到回复,则说明服务端已经正常关闭,这样客户端就可以关闭连接了。 > **13.HashSet和HashMap之间的关系** * HashMap:线程不安全,可通过j.u.c(java.util.concurrent)包中的ConcurrentHashMap解决(线程安全,适用于高并发)。JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体(默认初始容量为16),数组中的每个元素是链表的形式。JDK1.8以后,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 * HashSet(唯一,无序): 线程不安全,基于HashMap实现的,底层采用 HashMap 来保存元素,源码如下: ```Java public HashSet() { map = new HashMap<>(); } ``` * `hashmap.put(key,value);` 对于hashmap,在增加元素时,如果key已经存在,则将该key对应的value值覆盖掉,返回值是:在本次增加元素之前,key对应的value值(首次返回为null)。 * `hashset.add(key,常量值);`所有hashset中增加的Value都是一个常量值 `PRESENT`(new Object()) 对于hashset,在增加元素时,如果集合中不存在,则返回true;如果已经存在则返回false。 > **14.包装类** > > | 基本类型 | 包装类 | > | -------- | --------- | > | int | Integer | > | byte | Byte | > | short | Short | > | long | Long | > | float | Float | > | double | Double | > | boolean | Boolean | > | char | Character | `Interger i = 10;`//自动装箱 `int j = i;`//自动拆箱 `Integer + int`/`Integer == int` -> int; int优先高,自动拆箱 ```Java public class Wapper { public static void main(String[] args) { //自动装箱,int转Integer Integer f1 = 100, f2 = 100, f3 = 200, f4 = 200; //实际上,包装类型间的相等判断应该用equals,而不是'==' System.out.println(f1 == f2);//true System.out.println(f3 == f4);//false } } ``` 以下是Integer类中“自动装箱”的源码: ```Java public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } ``` 其中IntegerCache.low的是值是-128,IntegerCache.high的值是127。也就是说,Integer在自动装箱时,如果判断整数值的范围在[-128,127]之间,则直接使用整型常量池中的值;如果不在此范围,则会new 一个新的Integer()。因此,本题f1和f2都在[-128,127]范围内,使用的是常量池中同一个值。而f3和f4不在[-128,127]范围内,二者的值都是new出来的,因此f3和f4不是同一个对象。 > **15.泛型** ```Java A<x> z = new a<x>();//根据泛型约束,两个x类型必须一致,不能是父子关系。 List<Object> objs = new ArrayList<> ();//对,根据jdk新特性可以自动推测出后面应该为Object 相当于: List<Object> objs = new ArrayList<Object> (); //对 List<Object> objs = new ArrayList<String> ();//错 List<?> objs = new ArrayList<String> ();//对 相当于: List<? extends Object> objs = new ArrayList<String> ();//对 List<? extends String> objs = new ArrayList<String> ();//对,在考虑泛型时,不用考虑 final ``` > **16.异常分类** > 以下程序,能否成功运行? ```Java public class ExceptionDemo01 { void test() throws NullPointerException{ System.out.println("A..."); } public static void main(String[] args) { new ExceptionDemo01().test(); } } ```  异常分为 **运行异常**和**检查异常** * 运行异常:RuntimeException,运行时才会发现的异常(编译时不会进行检查)。 * 检查异常:CheckedException(泛指除了RuntimeException以外的其他所有异常):编译时会进行检查。 如果是CheckedException,在编写代码时,必须try..catch或者throws二选一。 但是RuntimeException在编写时不会进行异常检查,因此本题目中抛出的NullPointerException在编译阶段不会被检查。而在运行时,本题中并不会造成NullPointerException,因此无需任何处理,也不会报错。 简言之,本题目在编译阶段不会进行异常检查(本题是RuntimeException),而在运行时又不会发生异常,因此是正确的,会正常运行。 > **17.默认值** > 整数的默认类型是int,小数的默认类型是double。但是在赋值时(在数值范围内),`=`可以自动转为相应的整数类型(byte/short/int/long);但却不会转为小数类型,如下所示。 ```java byte num = 10 ;//正确,10会自动转为byte类型 float f = 3.4; //错误,3.14是double类型,不会自动转为float float f = (float) 3.4;//正确 ``` 3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于向下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。 > **18.volatile可以禁止指令的重排序功能。那么synchronized有这个功能吗?** > **重排序所实现的优化不会影响单线程程序执行结果** ```Java 1. int a = 100 ; 2. int b ; 3. b = 200 ; 4. int c = a * b ; ``` 根据重排序,以上代码的实际执行顺序可以是1、2、3、4,也可以是2、3、1、4,还可以是2、1、3、4等,因为这几种可能的最终执行结果都是相同的。(实际上第4句还可以再拆) 而synchronized的作用是加锁,可以保证串行执行,即可以让并发环境转为单线程环境。因此加了synchronized就已经是单线程环境了。既然是单线程,那么无论是否进行了重排序,最终的结果都不会有影响,即都可以保证线程安全。所以说,在使用synchronized时根本不用关心“重排序”这个问题,无论它支持或不支持,都已经不重要了。 > **19.“如果一个对象存在着指向它的引用,那么这个对象就不会被GC回收”,这句话对吗?** > 不对。JVM中存在着四种类型的引用:**强引用**、**软引用**、**弱引用**和**虚引用**。 这句话只适用于“强引用”,`Object ref = new Object()`中的ref就是一个强引用。但除此以外,还有以下三个: 软引用:当JVM的内存足够时,GC不会主动回收软引用对象;但当JVM的内存不足时,GC就会去主动回收软引用对象。 弱引用:当GC进行垃圾回收时,无论是否当时JVM的内存是否充足,都会去主动回收弱引用对象。 虚引用:是否使用虚引用对于一个对象本身来说都没有任何区别。虚引用的价值在于和引用队列一起使用。 综上,软引用、弱引用和虚引用都是这句话的反例。 > **20.JVM类加载问题 (待研究)** ```Java package com.yanqun.pojo; class MyClass{ static int num1 = 100 ; static MyClass myClass = new MyClass(); public MyClass(){ num1 = 200 ; num2 = 200 ; } static int num2 = 100 ; public static MyClass getMyClass(){ return myClass ; } @Override public String toString() { return this.num1 + "\t" + this.num2 ; } } public class MyClassLoader { public static void main(String[] args) { MyClass myc = MyClass.getMyClass() ; System.out.println(myc); } } ``` 运行结果:200 100 解析: JVM使用“类”的生命周期是: 类的加载->连接->初始化->使用->卸载 各阶段主要完成的工作如下: 1.类的加载: (1)寻找并加载类的二进制数据(class文件) (2)将硬盘上的class文件 加载到jvm内存中 2.连接 该阶段又包含了验证、准备和解析3个过程,如下。 (1)验证 校验.class文件的正确性 **(2)准备** **给static静态变量分配内存,并初始化static的默认值。** 因此,本题在此阶段各变量的值如下: ```Java static int num1 = 0 ; static MyClass myClass = null ; static int num2 = 0 ; ``` (3)解析:把类中符号引用,转为直接引用 举个例子,在加载阶段,JVM还不知道类的具体内存地址,只能使用“com.yanqun.pojo.MyClass ”来替代MyClass类,“com.yanqun.pojo.MyClass ”就称为符号引用;但在解析阶段,JVM就可以将 “com.yanqun.pojo.MyClass ”映射成实际的内存地址,因此就可以用 内存地址来代替MyClass,这种使用 内存地址来使用 类的方法 称为直接引用。 3.初始化:给static变量 赋予实际的值 因此,本题在此阶段各变量的值如下: ```Java static int num1 = 100 ; static MyClass myClass = new MyClass();此句调用了构造方法,构造方法会进行如下赋值: public MyClass(){ num1 = 200 ; num2 = 200 ; } static int num2 = 100 ; ``` **根据程序 自上而下执行的特点,num1最终的值是200,num2最终的值是100。** (4)使用:对象的初始化、对象的垃圾回收、对象的销毁 (5)卸载 Last modification:August 14, 2022 © Allow specification reprint Like 2 喵ฅฅ