找工作再也不愁之面试题全覆盖-Java基础篇

Posted 墨家巨子@俏如来

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了找工作再也不愁之面试题全覆盖-Java基础篇相关的知识,希望对你有一定的参考价值。

一.JavaSE 部分

基础篇

Java中基本数据类型有哪些?

byte:8位,最大存储数据量是255,存放的数据范围是-128~127之间。

short:16位,最[大数据]存储量是65536,数据范围是-32768~32767之间。

int:32位,最大数据存储容量是2的32次方减1,数据范围是负的2的31次方到正的2的31次方减1。

long:64位,最大数据存储容量是2的64次方减1,数据范围为负的2的63次方到正的2的63次方减1。

float:32位,数据范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F。

double:64位,数据范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。

boolean:只有true和false两个取值。

char:16位,存储Unicode码,用单引号赋值。

Integer 和 int的区别

int是基本数据类型,变量中直接存放数值,变量初始化时值是0

Integer是引用数据类型,变量中存放的是该对象的引用,变量初始化时值时null

Integer是int类型的包装类,将int封装成Integer,符合java面向对象的特性,可以使用各种方法比如和其他数据类型间的转换

Integer和int的深入对比:

  1. 两个通过new生成的Integer对象,由于在堆中地址不同,所以永远不相等

  2. int和Integer比较时,只要数值相等,结果就相等,因为包装类和基本数据类型比较时,会自动拆箱,将Integer转化为int

  3. 通过new生成的Integer对象和非通过new生成的Integer对象相比较时,由于前者存放在堆中,后者存放在Java常量池中,所以永远不相等

  4. 两个非通过new生成的Integer对象比较时,如果两个变量的数值相等且在-128到127之间,结果就相等。这是因为给Integer对象赋一个int值,java在编译时,会自动调用静态方法valueOf(),根据java api中对Integer类型的valueOf的定义,对于-128到127之间的整数,会进行缓存,如果下次再赋相同的值会直接从缓存中取,即享元模式

String和StringBuilder和StringBuffer区别

三者底层都是char[]存储数据,JDK1.9之后使用的是byte[] ,因为往往我们存储都是短字符串,使用byte[]这样更节约空间。

由于String底层的char[]有final修饰,因此每次对String的操作都会在内存中开辟空间,生成新的对象,所以String不可变

StringBuilder和StringBuffer是可变字符串,没有final修饰,适合字符串拼接,另外StringBuffer是线程安全的,方法有synchronized修饰,但是性能较低,StringBuilder是线程不安全的,方法没有synchronized修饰,性能较高

String a = “A” 和 String a = new String(“A”) 创建字符串的区别

String c = “A” 首先去常量池找 “A”,如果有,会把a指向这个对象的地址 ,如果没有则在栈中创建三个char型的值’A’,堆中创建一个String对象object,值为"A",接着object会被存放进字符串常量池中,最后将a指向这个对象的的地址

new String(“A”) : 如果常量池中么有“A”就会走上面相同的流程先创建“A”,然后在堆中创建一个String对象,它的值共享栈中已有的char值“A”。

下面代码创建了几个对象

  • String s = “a” +“b” + “c” + “d”;这条语句创建了几个对象?

创建了一个对象,因为相对于字符串常量相加的表达式,编译器会在编译期间进行优化,直接将其编译成常量相加的结果。

  • String s; 创建几个对象?
    没有创建对象。

  • String a = “abc”; String b = “abc”; 创建了几个对象

    创建了一个对象,只是在第一条语句中创建了一个对象,a和b都指向相同的对象"abc",引用不是对象

== 和 equals 的区别是什么

==比较对象比较的是地址,对于Object对象中的equals 方法使用的也是 == ,比较的是对象的地址,默认情况下使用对象的equals比较Object中的equals方法,也就是比较地址,如果要实现自己的比较方式需要复写equals 方法。

对于包装类比如:Integer都是复写过equals方法,比较的是int 值。

final 和 finally 和 finalize 的区别

当用final修饰类的时,表明该类不能被其他类所继承。当我们需要让一个类永远不被继承,此时就可以用final修饰

finally作为异常处理的一部分,它只能用在try/catch语句中,并且附带一个语句块,表示这段语句最终一定会被执行(不管有没有抛出异常),经常被用在需要释放资源的情况下

finalize()是在java.lang.Object里定义的,也就是说每一个对象都有这么个方法。这个方法在gc启动,该对象被回收的时候被调用。其实gc可以回收大部分的对象(凡是new出来的对象,gc都能搞定,一般情况下我们又不会用new以外的方式去创建对象),所以一般是不需要程序员去实现finalize的。

JDK 和 JRE 有什么区别?

JRE(Java Runtime Enviroment) :是Java的运行环境,JRE是运行Java程序所必须环境的集合,包含JVM标准实现及 Java核心类库

JDK(Java Development Kit) :是Java开发工具包,它提供了Java的开发环境(提供了编译器javac等工具,用于将java文件编译为class文件)和运行环境(提 供了JVM和Runtime辅助包,用于解析class文件使其得到运行)。JDK是整个Java的核心,包括了Java运行环境(JRE),一堆Java工具tools.jar和Java标准类库 (rt.jar)。

面向对象四大特性

抽象 : 是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面,抽象只关注对象的哪些属性和行为,并不关注这此行为的细节是什么 - 举例:定义一个persion类,了就是对这种事物的抽象

封装:对数据的访问只能通过已定义的接口,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口,比如在Java中,把不需要暴露的内容和实现细节隐藏起来,或者private修饰,然后提供专门的访问方法,如JavaBean。 - 生活举例:电脑主机就是把主板等封装到机壳,提供USB接口,网卡接口,电源接口等。 JavaBean就是一种封装。

继承:新类(子类,派生类)继承了原始类的特性,子类可以从它的父类哪里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要。

多态:多态是指允许不同类的对象对同一消息做出响应。对象的多种形态,当编译时类型和运行时类型不一样,就是多态,意义在于屏蔽子类差异

方法覆盖和重载

方法的覆盖是子类和父类之间的关系,方法的重载是同一个类中方法之间的关系。
覆盖只能由一个方法,或只能由一对方法产生关系;方法的重载是多个方法之间的关系。
覆盖要求参数列表相同;重载要求参数列表不同。

普通类和抽象类

抽象类不能被实例化, 需要通过子类实例化
抽象类可以有构造函数,被继承时子类必须继承父类一个构造方法,抽象方法不能被声明为静态。
抽象方法只需申明,而无需实现,抽象类中可以允许普通方法有主体
含有抽象方法的类必须申明为抽象类
抽象的子类必须实现抽象类中所有抽象方法,否则这个子类也是抽象类

接口和抽象类

定义接口使用interface,定义抽象类使用abstract class

接口由全局常量,抽象方法,(java8后:静态方法,默认方法)

抽象类由构造方法,抽象方法,普通方法

接口和类是实现关系,抽象类和类是继承关系


IO流

你知道BIO,NIO,AIO么?讲一下你的理解

BIO (Blocking I/O):同步阻塞I/O 模式,以流的方式处理数据,数据的读取写入必须阻塞在一个线程内等待其完成。适用于连接数目比较小且固定的架构

NIO (New I/O):同时支持阻塞与非阻塞模式,以块的方式处理数据,适用于连接数目多且连接比较短(轻操作)的架构,比如聊天器

AIO ( Asynchronous I/O):异步非阻塞I/O 模型,适用于连接数目多且连接比较长(重操作)的架构

java 中四大基础流

InputStream : 输入字节流, 也就是说它既属于输入流, 也属于字节流 ,

OutputStream: 输出字节流, 既属于输出流, 也属于字节流

Reader: 输入字符流, 既属于输入流, 又属于字符流

Writer: 输出字符流, 既属于输出流, 又属于字符流

读文本用什么流,读图片用什么流

文本用字符输入流,读图片用直接输入流

字符流和字节流有什么区别

字符流适用于读文本,字节流适用于读图片,视频,文件等。

字节流操作的基本单元为字节;字符流操作的基本单元为Unicode码元。

字节流默认不使用缓冲区;字符流使用缓冲区。

字节流通常用于处理二进制数据,实际上它可以处理任意类型的数据,但它不支持直接写入或读取Unicode码元;字符流通常处理文本数据,它支持写入及读取Unicode码元

BufferedInputStream 用到什么设计模式

主要运用了俩个设计模式,适配器和装饰者模式

带缓冲区的流

BufferedInputStream 带缓冲区的字节输入

BufferedOutputStream 带缓冲区的输出流

BufferedReader : 带缓冲区的字符输入流

BufferedWriter : 带缓冲区的字符输出流


集合篇

说一下Java中的集合体系

Collection接口

List:

  • ArrayList:底层数据结构是数组,查询性能高,增删性能低

  • Vector:底层数据结构是数组,查询性能高,增删性能低

  • LinkedList:底层数据结构是双向链表,查询性能低,增删性能高

Set:

  • HashSet:无序不重复的,使用HashMap的key存储元素,判断重复依据是hashCode()和equals()

  • TreeSet:有序不重复的,底层使用TreeMap的key存储元素,排序方式分为自然排序,比较器排序

Map接口

  • HashMap:key的值没有顺序,线程不安
  • TreeMap:key的值可以自然排序,线程不安全
  • HashTable:它的key和value都不允许为null,线程安全
  • Properties:它的key和value都是String类型的,线程安全

HashMap和HashTable的区别

HashMap和HashTable都是实现了Map接口的集合框架,他们的区别

  • HashTable是线程安全的,它的实现方法都加了synchronized关键字,因此它的性能较低

  • HashMap是线程不安全的,它实现方法没有加synchronized,因此它的性能较高

  • HashMap的key和value都允许为null,HashTable中的key和value都不能为null,如果不考虑线程安全,建议使用HashMap,如果需要考虑线程安全的高并发实现,建议使用ConcurrentHashMap

ArrayList和LinkedList区别

都属于线性结构,ArrayList是基于数组实现的,开辟的内存空间要求联系,可以根据索引随机访问元素性能高,但是插入和删除元素性能差,因为这会涉及到移位操作

LinkedList是基于双链表实现的,开配的内存空间不要求连续,因此不支持索引,查找元素需要从头查找,因此性能差,但是添加删除只需要改变指针指向即可,性能高. LinkedList会增加内存碎片化,增加内存管理难度

根据实际需要,如果项目中使用查找较多,使用ArrayList,如果使用增删较多,请使用LinkedList

ArrayList和Vector区别

ArrayList是线程不安全的,Vector相反是线程安全的,方法加了同步锁,线程安全但是性能差,ArrayList底层数组容量不足时,会自动扩容0.5倍,Vector会自动扩容1倍

一个User的List集合,如何实现根据年龄排序

第一种方式,让User类实现Comparable接口,覆写compareTo方法,方法中自定义根据年龄比较的算法

第二种方式,调用Collections.sort方法,传入一个比较器,覆写compare方法,方法中自定义根据年龄比较的算法

HashMap底层用到了那些数据结构?

JDK1.7及其之前:数组,链表 ; JDK1.8开始:数组,链表,红黑树

什么是Hash冲突

哈希冲突,也叫哈希碰撞,指的是两个不同的值,计算出了相同的hash,也就是两个不同的数据计算出同一个下标,通常解决方案有:

  • 拉链法,把哈希碰撞的元素指向一个链表

  • 开放寻址法,把产生冲突的哈希值作为值,再进行哈希运算,直到不冲突

  • 再散列法,就是换一种哈希算法重来一次

  • 建立公共溢出区,把哈希表分为基本表和溢出表,将产生哈希冲突的元素移到溢出表

HashMap为什么要用到链表结构

当我们向HashMap中添加元素时,会先根据key尽心哈希运算,把hash值模与数组长度得到一个下标,然后将该元素添加进去。但是如果产生了哈希碰撞,也就是不同的key计算出了相同的hash值,这就出问题了,因此它采用了拉链法来解决这个问题,将产生hash碰撞的元素,挂载到链表中

HashMap为什么要用到红黑树

当HashMap中同一个索引位置出现哈希碰撞的元素多了,链表会变得越来越长,查询效率会变得越来越慢。因此在JDK1.8之后,当链表长度超过8个,会将链表转坏为红黑树来提高查询

HashMap链表和红黑树在什么情况下转换的?

当链表的长度大于等于8,同时数组的长度大于64,链表会自动转化为红黑树,当树中的节点数小于等于6,红黑树会自动转化为链表

HashMap在什么情况下扩容?HashMap如何扩容的?

HashMap的数组初始容量是16,负载因子是0.75,也就是说当数组中的元素个数大于12个,会成倍扩容

tips:为啥子是0.75:负载因子过小容易浪费空间,过大容易造成更多的哈希碰撞,产生更多的链表和树,因此折衷考虑采用了0.75

为啥子是成倍扩容:需要保证数组的长度是2的整数次幂

为嘛数组的长度必须是2的整数次幂:我们在存储元素到数组中的时候,是通过hash值模与数组的长度,计算出下标的。但是由于计算机的运算效率,加减法>乘法>除法>取模,取模的效率是最低的。开发者们为了让你用的开心,也是呕心沥血。将取模运算转化成了与运算,即数组长度减1的值和hash值的与运算,以此来优化性能。但是这个转化有一个前提,就是数组的长度必须为2的整数次幂

HashMap是如何Put一个元素的

首先,将key进行hash运算,将这个hash值与上当前数组长度减1的值,计算出索引。此时判断该索引位置是否已经有元素了,如果没有,就直接放到这个位置

如果这个位置已经有元素了,也就是产生了哈希碰撞,那么判断旧元素的key和新元素的key的hash值是否相同,并且将他们进行equals比较,如果相同证明是同一个key,就覆盖旧数据,并将旧数据返回,如果不相同的话

再判断当前桶是链表还是红黑树,如果是红黑树,就按红黑树的方式,写入该数据,

如果是链表,就依次遍历并比较当前节点的key和新元素的key是否相同,如果相同就覆盖,如果不同就接着往下找,直到找到空节点并把数据封装成新节点挂到链表尾部。然后需要判断,当前链表的长度是否大于转化红黑树的阈值,如果大于就转化红黑树,最后判断数组长度是否需要扩容。

HashMap是如何Get一个元素的

首先将key进行哈希运算,计算出数组中的索引位置,判断该索引位置是否有元素,如果没有,就返回null,如果有值,判断该数据的key是否为查询的key,如果是就返回当前值的value

如果第一个元素的key不匹配,判断是红黑树还是链表,如果是红黑树,就就按照红黑树的查询方式查找元素并返回,如果是链表,就遍历并匹配key,让后返回value值

你知道HahsMap死循环问题吗

HashMap在扩容数组的时候,会将旧数据迁徙到新数组中,这个操作会将原来链表中的数据颠倒,比如a->b->null,转换成b->a->null

这个过程单线程是没有问题的,但是在多线程环境,就可能会出现a->b->a->b…,这就是死循环

在JDK1.8后,做了改进保证了转换后链表顺序一致,死循环问题得到了解决。但还是会出现高并发时数据丢失的问题,因此在多线程情况下还是建议使用ConcurrentHashMap来保证线程安全问题

说一下你对ConcurrentHashMap的理解

ConcurrentHashMap,它是HashMap的线程安全,支持高并发的版本

在jdk1.7中,它是通过分段锁的方式来实现线程安全的。意思是将哈希表分成许多片段Segment,而Segment本质是一个可重入的互斥锁,所以叫做分段锁。

在jdk1.8中,它是采用了CAS操作和synchronized来实现的,而且每个Node节点的value和next都用了volatile关键字修饰,保证了可见性


多线程

创建线程是几种方式

方式一:继承Thread类,覆写run方法,创建实例对象,调用该对象的start方法启动线程
方式二:创建Runnable接口的实现类,类中覆写run方法,再将实例作为此参数传递给Thread类有参构造创建线程对象,调用start方法启动

方式三:创建Callable接口的实现类,类中覆写call方法,创建实例对象,将其作为参数传递给FutureTask类有参构造创建FutureTask对象,再将FutureTask对象传递给Thread类的有参构造创建线程对象,调用start方法启动

Thread有单继承的局限性,Runnable和Callable三避免了单继承的局限,使用更广泛。Runnable适用于无需返回值的场景,Callable使用于有返回值的场景

Thread的start和run的区别

start是开启新线程, 而调用run方法是一个普通方法调用,还是在主线程里执行。没人会直接调用run方法

sleep 和 wait的区别

第一,sleep方法是Thread类的静态方法,wait方法是Object类的方法

第二:sleep方法不会释放对象锁,wait方法会释放对象锁

第三:sleep方法必须捕获异常,wait方法不需要捕获异常

线程的几种状态

新建状态:线程刚创建,还没有调用start方法之前

就绪状态:也叫临时阻塞状态,当调用了start方法后,具备cpu的执行资格,等待cpu调度器轮询的状态

运行状态:就绪状态的线程,获得了cpu的时间片,真正运行的状态

冻结状态:也叫阻塞状态,指的是该线程因某种原因放弃了cpu的执行资格,暂时停止运行的状态,比如调用了wait,sleep方法

死亡状态:线程执行结束了,比如调用了stop方法

Synchronized 和 lock的区别

他们都是用来解决并发编程中的线程安全问题的,不同的是

  • synchronized是一个关键字,依靠Jvm内置语言实现,底层是依靠指令码来实现;Lock是一个接口,它基于CAS乐观锁来实现的
  • synchronized在线程发生异常时,会自动释放锁,不会发生异常死锁,Lock在异常时不会自动释放锁,我们需要在finally中释放锁
  • synchronized是可重入,不可判断,非公平锁,Lock是可重入,可判断的,可手动指定公平锁或者非公平锁

你知道AQS吗

AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制,它维护了一个volatile修饰的 int 类型的,state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

工作思想是如果被请求的资源空闲,也就是还没有线程获取锁,将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果请求的资源被占用,就将获取不到锁的线程加入队列。

悲观锁和乐观锁

悲观锁和乐观锁,指的是看待并发同步问题的角度

  • 悲观锁认为,对同一个数据的并发操作,一定是会被其他线程同时修改的。所以在每次操作数据的时候,都会上锁,这样别人就拿不到这个数据。如果不加锁,并发操作一定会出问题。用阳间的话说,就是总有刁民想害朕

  • 乐观锁认为,对同一个数据的并发操作,是不会有其他线程同时修改的。它不会使用加锁的形式来操作数据,而是在提交更新数据的时候,判断一下在操作期间有没有其他线程修改了这个数据

悲观锁一般用于并发小,对数据安全要求高的场景,乐观锁一般用于高并发,多读少写的场景,通常使用版本号控制,或者时间戳来解决.

你知道什么是CAS嘛

CAS,compare and swap的缩写,中文翻译成比较并交换。它是乐观锁的一种体现,CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。

Synchronized 加非静态和静态方法上的区别

实例方法上的锁,锁住的是这个对象实例,它不会被实例共享,也叫做对象锁

静态方法上的锁,锁住的是这个类的字节码对象,它会被所有实例共享,也叫做类锁

Synchronized(this) 和 Synchronized (User.class)的区别

Synchronized(this) 中,this代表的是该对象实例,不会被所有实例共享

Synchronized (User.class),代表的是对类加锁,会被所有实例共享

Synchronized 和 volatitle 关键字的区别

这两个关键字都是用来解决并发编程中的线程安全问题的,不同点主要有以下几点

第一:volatile的实现原理,是在每次使用变量时都必须重主存中加载,修改变量后都必须立马同步到主存;synchronized的实现原理,则是锁定当前变量,让其他线程处于阻塞状态

第二:volatile只能修饰变量,synchronized用在修饰方法和同步代码块中

第三:volatile修饰的变量,不会被编译器进行指令重排序,synchronized不会限制指令重排序

第四:volatile不会造成线程阻塞,高并发时性能更高,synchronized会造成线程阻塞,高并发效率低

第五:volatile不能保证操作的原子性,因此它不能保证线程的安全,synchronized能保证操作的原子性,保证线程的安全

synchronized 锁的原理

synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实 现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖 底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低,涉及到用户态到内核态的切换,会让整个程序性能变得很差。

因此在JDK1.6及以后的版本中,增加了锁升级的过程,依次为无锁,偏向锁,轻量级锁,重量级锁。而且还增加了锁粗化,锁消除等策略,这就节省了锁操作的开销,提高了性能

synchronized 锁升级原理

每个对象都拥有对象头,对象头由Mark World ,指向类的指针,以及数组长度三部分组成,锁升级主要依赖Mark Word中的锁标志位和释放偏向锁标识位。

  • 偏向锁(无锁)

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程 获得锁之后(线程的id会记录在对象的Mark Word锁标志位中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。(第二次还是这个线程进来就不需要重复加锁,基本无开销),如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

  • 轻量级锁(CAS):

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁自旋锁);没有抢到锁的线程将自旋,获取锁的操作。轻量级锁的意图是在没有多线程竞争的情况下,通过CAS操作尝试将MarkWord锁标志位更新为指向LockRecord的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。

长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)

  • 重量级锁:

如果锁竞争情况严重,某个达到最大自旋次数(10次默认)的线程,会将轻量级锁升级为重量级锁,重量级锁则直接将自己挂起,在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。

虚拟机使用CAS操作尝试将MarkWord更新为指向LockRecord的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。

乐观锁的使用场景(数据库,ES)

场景一:ES中对version的控制并发写。

场景二:数据库中使用version版本号控制来防止更新覆盖问题。

场景三:原子类中的CompareAndSwap操作

AtomicInterger怎么保证并发安全性的

通过CAS操作原理来实现的,就可见性和原子性两个方面来说

它的value值使用了volatile关键字修饰,也就保证了多线程操作时内存的可见性

Unsafe这个类是一个很神奇的类,而compareAndSwapInt这个方法可以直接操作内存,依靠的是C++来实现的,它调用的是Atomic类的cmpxchg函数。而这个函数的实现是跟操作系统有关的,比如在X86的实现就利用汇编语言的CPU指令lock cmpxchg,它在执行后面的指令时,会锁定一个北桥信号,最终来保证操作的原子性

什么是重入锁,什么是自旋锁,什么是阻塞

可重入锁是指允许同一个线程多次获取同一把锁,比如一个递归函数里有加锁操作

自旋锁不是锁,而是一种状态,当一个线程尝试获取一把锁的时候,如果这个锁已经被占用了,该线程就处于等待状态,并间隔一段时间后再次尝试获取的状态,就叫自旋

阻塞,指的是当一个线程尝试获取锁失败了,线程就就进行阻塞,这是需要操作系统切换CPU状态的

你用过JUC中的类吗,说几个

Lock锁体系 ,ConcurrentHashMap ,Atomic原子类,如:AtomicInteger ;ThreadLoal ; ExecutorService

ThreadLocal的作用和原理

ThreadLocal,翻译成中国话,叫做线程本地变量,它是为了解决线程安全问题的,它通过为每个线程提供一个独立的变量副本,来解决并发访问冲突问题 - 简单理解它可以把一个变量绑定到当前线程中,达到线程间数据隔离目的。

原理:ThredLocal是和当前线程有关系的,每个线程内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,它用来存储每个线程中的变量副本,key就是ThreadLocal变量,value就是变量副本。

当我们调用get方法是,就会在当前线程里的threadLocals中查找,它会以当前ThreadLocal变量为key获取当前线程的变量副本

它的使用场景比如在spring security中,我们使用SecurityContextHolder来获取SecurityContext,比如在springMVC中,我们通过RequestContextHolder来获取当前请求,比如在 zuul中,我们通过ContextHolder来获取当前请求

线程池的作用

请求并发高的时候,如果没有线程池会出现线程频繁创建和销毁而浪费性能的情况,同时没办法控制请求数量,所以使用了线程池后有如下好处

  • 主要作用是控制并发数量,线程池的队列可以缓冲请求
  • 线程池可以实现线程的复用效果
  • 使用线程池能管理线程的生命周期

Executors创建四种线程池

  • CachedThreadPool:可缓存的线程池,它在创建的时候,没有核心线程,线程最大数量是Integer最大值,最大空闲时间是60S

  • FixedThreadPool:固定长度的线程池,它的最大线程数等于核心线程数,此时没有最大空闲时长为0

  • SingleThreadPool:单个线程的线程池,它的核心线程和最大线程数都是1,也就是说所有任务都串行的执行

  • ScheduledThreadPool:可调度的线程池,它的最大线程数是Integer的最大值,默认最长等待时间是10S,它是一个由延迟执行和周期执行的线程池

线程池的执行流程

corePoolSize,maximumPoolSize,workQueue之间关系。

  1. 当线程池中线程数小于corePoolSize时,新提交任务将创建一个新线程(使用核心)执行任务,即使此时线程池中存在空闲线程。
  2. 当线程池中线程数达到corePoolSize时(核心用完),新提交任务将被放入workQueue中,等待线程池中任务调度执行 。
  3. 当workQueue已满,且maximumPoolSize > corePoolSize时,新提交任务会创建新线程(非核心)执行任务。
  4. 当workQueue已满,且提交任务数超过maximumPoolSize(线程用完,队列已满),任务由RejectedExecutionHandler处理。
  5. 当线程池中线程数超过corePoolSize,且超过这部分的空闲时间达到keepAliveTime时,回收这些线程。
  6. 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收。

线程池执行流程 : 核心线程 => 等待队列 => 非核心线程 => 拒绝策略

线程池构造器的7个参数

  • CorePoolSize:核心线程数,它是不会被销毁的

  • MaximumPoolSize :最大线程数,核心线程数+非核心线程数的总和

  • KeepAliveTime:非核心线程的最大空闲时间,到了这个空闲时间没被使用,非核心线程销毁

  • Unit:空闲时间单位

  • WorkQueue:是一个BlockingQueue阻塞队列,超过核心线程数的任务会进入队列排队

  • ThreadFactory:它是一个创建新线程的工厂

  • Handler:拒绝策略,任务超过最大线程数+队列排队数 ,多出来的任务该如何处理取决于Handler

线程池拒绝策略有几种

拒绝策略,当线程池任务超过 最大线程数+队列排队数 ,多出来的任务该如何处理取决于Handler

  1. AbortPolicy丢弃任务并抛出RejectedExecutionException异常;
  2. DiscardPolicy丢弃任务,但是不抛出异常;
  3. DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务;
  4. CallerRunsPolicy由调用线程处理该任务

可以定义和使用其他种类的RejectedExecutionHandler类来定义拒绝策略。

你知道ScheduledThreadPool使用场景吗

这是带定时任务的线程池,EurekaClient拉取注册表&心跳续约就是使用的这个线程池。

JVM篇

你们用什么工具监控JVM

jconsule, jvisualvm

JVM类加载流程

loading加载:class文件从磁盘加载到内存中

verification验证:校验class文件,包括字节码验证,元数据验证,符号引用验证等等

preparation准备:静态变量赋默认值,只有final会赋初始值

resolution解析:常量池中符号引用,转换成直接访问的地址

initializing初始化:静态变量赋初始值

JVM类加载器有几种类型,分别加载什么东西,用到什么设计模式?

  1. BootStrap ClassLoader 启动类加载器,加载<JAVA_HOME>\\lib下的类

  2. Extenstion ClassLoader 扩展类加载器,加载<JAVA_HOME>\\lib\\ext下的类

  3. Application ClassLoader 应用程序类加载器,加载Classpath下的类

  4. 自定义类加载器

这里是用到了双亲委派模式,从上往下加载类,在这过程中只要上一级加载到了,下一级就不会加载了,这麽做的目的

  • 不让我们轻易覆盖系统提供功能
  • 也要让我们扩展我们功能。

JVM组成,以及他们的作用

运行时数据区:

  • 堆:存放对象的区域,所有线程共享

  • 虚拟机栈:对应一个方法,线程私有的,存放局部变量表,操作数栈,动态链接等等

  • 本地方法栈:对应的是本地方法,在hotspot中虚拟机栈和本地方法栈是合为一体的

  • 程序计数器:确定指令的执行顺序

  • 方法区:存放虚拟机加载的类的信息,常量,静态变量等等,JDK1.8后,改为元空间

执行引擎:

  • 即时编译器,用来将热点代码编译成机器码(编译执行)

  • 垃圾收集,将没用的对象清理掉

本地方法库:融合不同的编程语言为java所用

在JVM层面,一个线程是如何执行的

线程执行,每个方法都会形成一个栈帧进行压榨保存到虚拟机栈中,方法调用结束就回出栈。调用过程中创建的变量在虚拟机栈,对象实例存放在堆内存中,栈中的变量指向了对中的内存。当方法执行完成就出栈,创建的变量会被销毁,堆中的对象等待GC。

程序内存溢出了,如何定位问题出在哪儿?

增加启动参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:\\ 可以把内存溢出的日志输出到文件,然后通过JVM监视工具VisualVM来分析日志,定位错误所在。在linux服务器也可以使用命令: jmap -dump 来下载堆快照。

垃圾标记算法

垃圾标记算法有:引用计数和可达性算法

  • 引用计数 : 给每一个对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;每当有一个地方不再引用它时,计数器值减1,这样只要计数器的值不为0,就说明还有地方引用它,它就不是无用的对象. 这种算法的问题是当某些对象之间互相引用时,无法判断出这些对象是否已死
  • GC Roots :找到一个对象作为 CG Root , 当一个对象到GC Roots没有任何引用链相连(GC Roots到这个对象不可达)时,就说明此对象是不可用的

垃圾回收算法

  • 标记清除算法 :分为标记和清除两个阶段,首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象 ;缺点:标记和清除两个过程效率都不高;标记清除之后会产生大量不连续的内存碎片。

  • 复制算法 :把内存分为大小相等的两块,每次存储只用其中一块,当这一块用完了,就把存活的对象全部复制到另一块上,同时把使用过的这块内存空间全部清理掉,往复循环 ,缺点:实际可使用的内存空间缩小为原来的一半,比较适合

  • 标记整理算法 :先对可用的对象进行标记,然后所有被标记的对象向一段移动,最后清除可用对象边界以外的内存

  • 分代收集算法 :把堆内存分为新生代和老年代,新生代又分为Eden区、From Survivor和To Survivor。一般新生代中的对象基本上都是朝生夕灭的,每次只有少量对象存活,因此新生代采用复制算法,只需要复制那些少量存活的对象就可以完成垃圾收集;老年代中的对象存活率较高,就采用标记-清除和标记-整理算法来进行回收。

垃圾回收器有哪些

  • 新生代:Serial :一款用于新生代的单线程收集器,采用复制算法进行垃圾收集。Serial进行垃圾收集时,不仅只用一条线程执行垃圾收集工作,它在收集的同时,所有的用户线程必须暂停(Stop The World

  • 新生代:ParNew : ParNew就是一个Serial的多线程版本`,其它与Serial并无区别。ParNew在单核CPU环境并不会比Serial收集器达到更好的效果,它默认开启的收集线程数和CPU数量一致,可以通过-XX:ParallelGCThreads来设置垃圾收集的线程数。

  • 新生代:Parallel Scavenge(掌握) Parallel Scavenge也是一款用于新生代的多线程收集器,与ParNew的不同之处是,ParNew的目标是尽可能缩短垃圾收集时用户线程的停顿时间,Parallel Scavenge的目标是达到一个可控制的吞吐量.Parallel Old收集器以多线程,采用标记整理算法进行垃圾收集工作。

  • 老年代:Serial Old ,Serial Old收集器是Serial的老年代版本,同样是一个单线程收集器,采用标记-整理算法。

  • 老年代CMS收集器是一种以最短回收停顿时间为目标的收集器,以“最短用户线程停顿时间”著称。整个垃圾收集过程分为4个步骤

    • 初始标记:标记一下GC Roots能直接关联到的对象,速度较快
    • 并发标记:进行GC Roots Tracing,标记出全部的垃圾对象,耗时较长
    • 重新标记:修正并发标记阶段引用户程序继续运行而导致变化的对象的标记记录,耗时较短
    • 并发清除:用标记-清除算法清除垃圾对象,耗时较长

    整个过程耗时最长的并发标记和并发清除都是和用户线程一起工作,所以从总体上来说,CMS收集器垃圾收集可以看做是和用户线程并发执行的。

  • 老年代:Parallel Old ,Parallel Old收集器是Parallel Scavenge的老年代版本,是一个多线程收集器,采用标记-整理算法。可以与Parallel Scavenge收集器搭配,可以充分利用多核CPU的计算能力

  • 堆收集:G1 收集器, G1 收集器是jdk1.7才正式引用的商用收集器,现在已经成为jdk1.9默认的收集器。前面几款收集器收集的范围都是新生代或者老年代,G1进行垃圾收集的范围是整个堆内存,它采用“化整为零”的思路,把整个堆内存划分为多个大小相等的独立区域(Region)在每个Region中,都有一个Remembered Set来实时记录该区域内的引用类型数据与其他区域数据的引用关系(在前面的几款分代收集中,新生代、老年代中也有一个Remembered Set来实时记录与其他区域的引用关系),在标记时直接参考这些引用关系就可以知道这些对象是否应该被清除,而不用扫描全堆的数据

Jdk1.7.18新生代使用Parallel Scavenge,老年代使用Parallel Old

Minor GC和Full GC

新生代的回收称为Minor GC,新生代的回收一般回收很快,采用复制算法,造成的暂停时间很短 ,而Full GC一般是老年代的回收,并伴随至少一次的Minor GC,新生代和老年代都回收,而老年代采用标记-整理算法这种GC每次都比较慢造成的暂停时间比较长`,通常是Minor GC时间的10倍以上。尽量减少 Full GC

JVM优化的目的是什么?

优化程序的内存使用大小,以及减少CG来减少程序的停顿来提升程序的性能。

堆怎么调,栈怎么调

-Xms : 初始堆,1/64 物理内存

-Xmx : 最大堆,1/4物理内存

-Xmn :新生代大小

-Xss : 栈大小

以上是关于找工作再也不愁之面试题全覆盖-Java基础篇的主要内容,如果未能解决你的问题,请参考以下文章

找工作再也不愁之面试题全覆盖-数据库篇

找工作再也不愁之面试题全覆盖-框架篇

找工作再也不愁之面试题全覆盖-项目相关

找工作再也不愁之面试题全覆盖-设计模式&数据结构

找工作再也不愁之面试技巧全覆盖-你应该这样写简历

找工作再也不愁之面试技巧全覆盖-这样面试保证拿Offer