【Java】几道让你拿offer的面试题

前言

只有光头才能变强html

以前在刷博客的时候,发现一些写得比较好的博客都会默默收藏起来。最近在查阅补漏,有的知识点比较重要的,可是在以前的博客中尚未写到,因而趁着闲整理一下。java

文本的知识点:git

  • Integer常量池
  • TCP拆包粘包
  • select、poll、epoll简单区别
  • jdk1.6之后对Synchronize锁优化
  • Java内存模型

本文力求简单讲清每一个知识点,但愿你们看完能有所收获github

1、神奇的Integer

前阵子在群上看有人在讨论关于Integer的true或者false问题,我本觉得我已经懂了这方面的知识点了。但仍是作错了,后来去请教了一下朋友。朋友又给我发了另外一张图:面试

后来发现这是出自《深刻理解Java虚拟机——JVM高级特性与最佳实践(第2版)》中的10.3.2小节中~c#

public class Main_1 {
    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
        System.out.println(c == d);
        System.out.println(e == f);
        System.out.println(c == (a + b));
        System.out.println(c.equals(a + b));
        System.out.println(g == (a + b));
        System.out.println(g.equals(a + b));
		System.out.println(g.equals(a + h));
    }

}
复制代码

大家能够先思考一下再往下翻看答案,看看能不能作对。缓存

1.1解题思路

在解这道题以前,相信不少人都已经知道了,在Java中会有一个Integer缓存池,缓存的大小是:-128~127安全

答案是:微信

  • true
  • false
  • true
  • true
  • true
  • false
  • true

简单解释一下:markdown

  • 使用==的状况:
    • 若是比较Integer变量,默认比较的是地址值
    • Java的Integer维护了从-128~127的缓存池
    • 若是比较的某一边有操做表达式(例如a+b),那么比较的是具体数值
  • 使用equals()的状况:
    • 不管是Integer仍是Long中的equals()默认比较的是数值
    • Long的equals()方法,JDK的默认实现:会判断是不是Long类型
  • 注意自动拆箱,自动装箱问题。

反编译一下看看:

import java.io.PrintStream;

public class Main_1 {
    public static void main(String[] paramArrayOfString) {
        Integer localInteger1 = Integer.valueOf(1);
        Integer localInteger2 = Integer.valueOf(2);
        Integer localInteger3 = Integer.valueOf(3);
        Integer localInteger4 = Integer.valueOf(3);
        Integer localInteger5 = Integer.valueOf(321);
        Integer localInteger6 = Integer.valueOf(321);
        Long localLong = Long.valueOf(3L);

        // 缓存池
        System.out.println(localInteger3 == localInteger4);
        
        // 超出缓存池范围
        System.out.println(localInteger5 == localInteger6);
        
        // 存在a+b数值表达式,比较的是数值
        System.out.println(localInteger3.intValue() == localInteger1.intValue() + localInteger2.intValue());

        // equals比较的是数值
        System.out.println(localInteger3.equals(Integer.valueOf(localInteger1.intValue() + localInteger2.intValue())));
        // 存在a+b数值表达式,比较的是数值
        System.out.println(localLong.longValue() == localInteger1.intValue() + localInteger2.intValue());
        // Long的equals()先判断传递进来的是否是Long类型,而a+b自动装箱的是Integer类型
        System.out.println(localLong.equals(Integer.valueOf(localInteger1.intValue() + localInteger2.intValue())));

		// ... 最后一句在这里漏掉了,你们应该能够推断出来
    }
}
复制代码

我使用的反编译工具是jd-gui,若是尚未试过反编译的同窗能够下载来玩玩:

2、Synchronize锁优化手段有哪些

多线程文章回顾:

以前在写多线程文章的时候,简单说了一下synchronized锁在jdk1.6之后会有各类的优化:适应自旋锁,锁消除,锁粗化,轻量级锁,偏向锁。

本觉得这些优化是很是难以理解的东西,其实否则~~~简单了解一下仍是很好理解的。

2.1适应自旋锁

锁竞争是kernal mode下的,会通过user mode(用户态)到kernal mode(内核态) 的切换,是比较花时间的。

自旋锁出现的缘由是人们发现大多数时候锁的占用只会持续很短的时间,甚至低于切换到kernal mode所花的时间,因此在进入kernal mode前让线程等待有限的时间,若是在此时间内可以获取到锁就避免了不少无谓的时间,若不能则再进入kernal mode竞争锁。

在JDK 1.6中引入了自适应的自旋锁,说明自旋的时间不固定,要不要自旋变得愈来愈聪明

自旋锁在JDK1.4.2中就已经引入,只不过默认是关闭的,可使用-XX:+UseSpinning参数来开启,在JDK1.6中就已经改成默认开启了。

参考资料:

2.2锁消除

若是JVM明显检测到某段代码是线程安全的(言外之意:无锁也是安全的),JVM会安全地原有的锁消除掉!

好比说:

public void vectorTest(){
        Vector<String> vector = new Vector<String>();
        for(int i = 0 ; i < 10 ; i++){
            vector.add(i + "");
        }

        System.out.println(vector);
    }
复制代码

Vector是默认加锁的,但JVM若是发现vector变量仅仅在vectorTest()方法中使用,那该vector是线程安全的。JVM会把vector内部加的锁去除,这个优化就叫作:锁消除。

2.3锁粗化

默认状况下,老是推荐将同步块的做用范围限制得尽可能小

可是若是一系列的连续操做都对同一个对象反复加锁和解锁,甚至加锁操做是出如今循环体中的,频繁地进行互斥同步操做也会致使没必要要的性能损耗

JVM会将加锁的范围扩展(粗化),这就叫作锁粗化。

2.4轻量级锁

轻量级锁能提高程序同步性能的依据是**“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”**,这是一个经验数据。

  • 若是没有竞争,轻量级锁使用CAS操做避免了使用互斥量的开销
  • 但若是存在锁竞争,除了互斥量的开销外,还额外发生了CAS操做,所以在有竞争的状况下,轻量级锁会比传统的重量级锁更慢。

简单来讲:若是发现同步周期内都是不存在竞争,JVM会使用CAS操做来替代操做系统互斥量。这个优化就被叫作轻量级锁。

2.5偏向锁

偏向锁就是在无竞争的状况下把整个同步都消除掉,连CAS操做都不作了

偏向锁能够提升带有同步但无竞争的程序性能。它一样是一个带有效益权衡(Trade Off)性质的优化,也就是说,它并不必定老是对程序运行有利,若是程序中大多数的锁老是被多个不一样的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数-XX:-UseBiasedLocking来禁止偏向锁优化反而能够提高性能。

2.6简单总结各类锁优化

  • 自适应偏向锁:自旋时间不固定
  • 锁消除:若是发现代码是线程安全的,将锁去掉
  • 锁粗化:加锁范围太小(重复加锁),将加锁的范围扩展
  • 轻量级锁:在无竞争的状况下使用CAS操做去消除同步使用的互斥量
  • 偏向锁:在无竞争环境下,把整个同步都消除,CAS也不作。

参考资料:

3、TCP粘包,拆包

这是在看wangjingxin大佬面经的时候看到的面试题,以前对TCP粘包,拆包没什么概念,因而就简单去了解一下。

3.1什么是拆包粘包?为何会出现?

在进行Java NIO学习时,可能会发现:若是客户端接二连三的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一块儿的状况。

TCP的首部格式:

  • TCP是基于字节流的,虽然应用层和TCP传输层之间的数据交互是大小不等的数据块,可是TCP把这些数据块仅仅当作一连串无结构的字节流,没有边界
  • 从TCP的帧结构也能够看出,在TCP的首部没有表示数据长度的字段

基于上面两点,在使用TCP传输数据时,才有粘包或者拆包现象发生的可能。

一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包

接收端收到了两个数据包,可是这两个数据包要么是不完整的,要么就是多出来一块,这种状况即发生了拆包和粘包

拆包和粘包的问题致使接收端在处理的时候会很是困难(由于没法区分一个完整的数据包)

3.2解决拆包和粘包

分包机制通常有两个通用的解决方法:

  • 1,特殊字符控制
  • 2,在包头首都添加数据包的长度

若是使用netty的话,就有专门的编码器和解码器解决拆包和粘包问题了。

tips:UDP没有粘包问题,可是有丢包和乱序。不完整的包是不会有的,收到的都是彻底正确的包。传送的数据单位协议是UDP报文或用户数据报,发送的时候既不合并,也不拆分。

参考资料

4、select、poll、epoll简单区别

NIO回顾:

在Linux下它是这样子实现I/O复用模型的:

调用select/poll/epoll其中一个函数,传入多个文件描述符,若是有一个文件描述符就绪,则返回,不然阻塞直到超时。

这几个函数是有些区别的,可能有的面试官会问到这三个函数究竟有什么区别:

区别以下图:

两句话总结:

  • select和poll都须要轮询每一个文件描述符,epoll基于事件驱动,不用轮询
  • select和poll每次都须要拷贝文件描述符,epoll不用
  • select最大链接数受限,epoll和poll最大链接数不受限

tips:epoll在内核中的实现,用红黑树管理事件块

4.1通俗例子

如今3y在公司里边实习,写完的代码须要给测试测一遍。

select/poll状况:

  • 开发在写代码,此时测试挨个问全部开发者,你写好程序了没有?要测试吗?

epoll状况:

  • 开发写完代码了,告诉测试:“我写好代码了,你去测测,功能是XXX”。因而测试高高兴兴去找bug了。

其余通俗描述[1]:

一个酒吧服务员(一个线程),前面趴了一群醉汉,忽然一个吼一声“倒酒”(事件),你小跑过去给他倒一杯,而后随他去吧,忽然又一个要倒酒,你又过去倒上,就这样一个服务员服务好多人,有时没人喝酒,服务员处于空闲状态,能够干点别的玩玩手机。至于epoll与select,poll的区别在于后二者的场景中醉汉不说话,你要挨个问要不要酒,没时间玩手机了。io多路复用大概就是指这几个醉汉共用一个服务员。

来源:

其余通俗描述[2]:

简单举个例子(可能也不是很形象)select/poll饭店服务员(内核)告诉饭店老板(用户程序):”如今有客人结帐“可是这个服务员没人明确告诉老板,哪几桌的客人结账。老板得自儿一个一个桌子去问:请问是你要结账?epoll饭店服务员(内核)告诉饭店老板(用户程序):”1,2,5号客人结帐“老板就能够直接去1,2,5号桌收钱了

来源:

深刻了解参考资料:

5、Java内存模型

JVM博文回顾:

以前在写JVM的时候,还一度把JVM内存结构与Java内存模型给搞混了~~~还好有热心的网友给我指出来。

JVM内存结构:

Java内存模型:

操做变量时的规则:

  • Java内存模型规定了全部的变量都存储在主内存
  • 线程的工做内存中保存了被该线程使用到的变量的主内存副本拷贝
  • 线程对变量的全部操做(读取、赋值等)都必须在工做内存中进行,而不能直接读写主内存中的变量

工做内存同步回主内存实现是经过如下的8种操做来完成:

  • lock(锁定):做用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):做用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定。
  • read(读取):做用于主内存变量,把一个变量值从主内存传输到线程的工做内存中,以便随后的load动做使用
  • load(载入):做用于工做内存的变量,它把read操做从主内存中获得的变量值放入工做内存的变量副本中。
  • use(使用):做用于工做内存的变量,把工做内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个须要使用变量的值的字节码指令时将会执行这个操做。
  • assign(赋值):做用于工做内存的变量,它把一个从执行引擎接收到的值赋值给工做内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操做。
  • store(存储):做用于工做内存的变量,把工做内存中的一个变量的值传送到主内存中,以便随后的write的操做。
  • write(写入):做用于主内存的变量,它把store操做从工做内存中一个变量的值传送到主内存的变量中。

Java内存模型是围绕着在并发过程当中如何处理原子性、可见性和有序性这3个特征来创建的

保证原子性的操做:

  • read、load、assign、use、store和write
  • synchronized锁

保证有序性(重排序致使无序)的操做:

  • volatile
  • synchronized锁

保证可见性:

  • volatile
  • synchronized锁
  • final

在上面也说了,有序性能够经过volatile和synchronized锁来保证,但咱们通常写程序的时候不会老是关注代码的有序性的。其实,咱们Java内部中有一个原则,叫作先行发生原则(happens-before)

  • “先行发生”(happens-before)原则能够经过:几条规则一揽子地解决并发环境下两个操做之间是否可能存在冲突的全部问题
  • 有了这些规则,而且咱们的操做是在这些规则定义的范围以内。咱们就能够确保,A操做确定比B操做先发生(不会出现重排序的问题)

“先行发生”(happens-before)原则有下面这么几条:

  • 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操做先行发生于书写在后面的操做。准确地说,应该是控制流顺序而不是程序代码顺序,由于要考虑分支、循环等结构。
  • 管程锁定规则(Monitor Lock Rule):一个unlock操做先行发生于后面对同一个锁的lock操做。这里必须强调的是同一个锁,而“后面”是指时间上的前后顺序。
  • volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操做先行发生于后面对这个变量的读操做,这里的“后面”一样是指时间上的前后顺序。线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每个动做。
  • 线程终止规则(Thread Termination Rule):线程中的全部操做都先行发生于对此线程的终止检测,咱们能够经过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  • 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,能够经过Thread.interrupted()方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
  • 传递性(Transitivity):若是操做A先行发生于操做B,操做B先行发生于操做C,那就能够得出操做A先行发生于操做C的结论。

参考资料:

6、最后

本文简单整理了一下在学习中作的笔记,还有在网上遇到一些比较重要的知识点(面试题)~但愿你们看完能有所收益。

参考资料:

  • 《深刻理解Java虚拟机——JVM高级特性与最佳实践(第2版)》

若是你们有更好的理解方式或者文章有错误的地方还请你们不吝在评论区留言,你们互相学习交流~~~

若是想看更多的原创技术文章,欢迎你们关注个人微信公众号:Java3y。公众号还有海量的视频资源哦,关注便可免费领取。

可能感兴趣的连接: