接下里几篇文章,都是我在去年秋招中遇到过的java真实面试题。这里作了一下总结。主要分为Java基础部分、JVM部分、数据结构与算法部分、计算机网络部分、操作系统部分、数据库部分、中间件部分、Spring框架部分以及大数据面试题。其中的问题的回答都是我在面试中的回答,有对有错。写这几篇文章的目的在于和大家多多交流,也希望大家能多多指正问题。
**
Java基础部分
**
1.CAS算法中的ABA问题怎么去解决?
使用AtomicStampedReference,即对象引用类型的原子类。通过其中的版本号增加机制,每改变一次对象就增加版本号的值。在改变对象时,比较当前线程记录下的版本号与对象自己的版本号。如果相同则则允许改变,否则不允许改变。
2.Java中的内部类
Map中的Entry<K,V>、ArrayList中的迭代器都是内部类以及ThreadPoolExecutor中的四大拒绝策略都是内部类。
优点/好处:
- 内部类可以访问该类定义所在作用域中的数据,包括private修饰的 -----无条件访问外部类元素
- 内部类对同一包内的其他类隐藏 -----实现隐藏,安全性
使用private或者protected修饰符来修饰内部类,就可以实现对外隐藏。 - 内部类+接口可以实现多继承 -----实现多继承
- 通过匿名内部类可以优化简单的接口实现
内部类是有可能引起内存泄漏的问题的
如果当内部类的引用被外部类以外的其他类引用时,就会造成内部类和外部类无法被GC回收的情况,即使外部类没有被引用,因为内部类持有指向外部类的引用)。
3.Java中对于泛型的理解
为什么使用泛型?
- 可以做到代码复用,减少代码的重复
- 保证类型的安全,在一个集合中添加类型不同的元素时,会在编译期就提示出错误
- 编程的便利
泛型,即”参数化类型”。怎么理解呢?就是把原本传入方法中参数的具体类型变为一种参数,类似于方法中的变量参数。
比如java的集合类中就大量使用到了泛型编程,还有java8的新特性函数式接口也用到了。
泛型擦除: Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
4.Java中StringBuffer和StringBuilder的底层
public StringBuffer() {
super(16); // 初始容量是16,创建一个字节数组,长度是16
}
//StringBuffer底层是通过synchronized关键字来实现线程安全
@Override
public synchronized int length() {
return count;
}
//线程不安全,但是效率比StringBuffer高
public StringBuilder() {
super(16); //StringBuilder初始容量同样为16,底层是一个字节数组
}
5.在重写toString()方法中不要使用this关键字,否则会产生递归调用
编译器看到String后面跟着一个“+”,而再后面的对象不是String时,于是编译器试着将这个对象转换成String类型(调用该对象的toString()方法),所以当后面跟的是this关键字时,编译器调用this的toString(),于是就发生了递归调用。
@Override
public String toString() {
return this+” ”;
}
由于this后面跟了个+号,所以会自动调用this的toString()方法。而this的toString()方法中
又遇到了this+” ”,因此会无限的调用,会产生StackOverFlow。
6.有没有使用过Lambda技术?
使用:
在创建线程时使用,该线程主要用于处理用户注册的过程。
new Thread(()->{
for(int i = 0; i < 5; i++){
System.out.println(i);
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"线程A").start();
还有可以快速简洁的遍历一个集合
List<Integer> nums = Arrays.asList(1,2,3,4);
nums.forEach(System.out::println);
forEach中传递的是个Consumer函数式接口
普通的比较器也可以用lambda表达式
PriorityQueue<Integer> heap = new PriorityQueue<Integer>((n1,n2) -> n1 - n2);
7.ThreadPoolExecutor执行execute方法有哪些过程?
①首先会判断核心线程池里是否有线程可执行,有空闲线程则创建一个线程来执行任务。
②当核心线程池里已经没有线程可执行的时候,此时将任务丢到任务队列中去。
③如果任务队列(有界)也已经满了的话,但运行的线程数小于最大线程池的数量的时候,此时将会新建一个线程用于执行任务,但如果运行的线程数已经达到最大线程池的数量的时候,此时将无法创建线程执行任务。然后按照指定的拒绝策略来进行安排。
8.Java中怎么解决出现多个if…else if…else的情况
9.父类和子类中静态变量和及静态代码块的访问
父类静态代码块>子类静态代码块>父类构造函数>子类构造函数
public class Person {
static {
System.out.println("Person内的静态代码块");
}
public Person(){
System.out.println("Person的构造函数");
}
int num1 = 5;
static int num2 = 15;
final int num5 = 60;
}
public class teacher extends Person {
static {
System.out.println("teacher内的静态代码块");
}
public teacher(){
System.out.println("teacher的构造函数");
}
int num1 = 10;
static int num3 = 20;
final static int num4 = 50;
}
//Person p1 = new teacher(); //加载父类静态代码块,父类构造函数
//teacher t1 = new teacher(); //1.父静态代码块2.子静态代码块
// 3. 父构造函数 4.子构造函数
//System.out.println(p1.num1 +"===="+t1.num1); // 5 ==== 10
//System.out.println(teacher.num2); // 子类可以访问父类中的静态变量,而且不会触发父类和子类的初始化,只会触发父类中的静态代码块
//System.out.println(teacher.num3); // 子类可以用类名来访问自己类中的静态变量,会触发父类和子类的静态初始化代码块,不会触发父类和子类的构造函数
//System.out.println(teacher.num4); // 子类访问自己类中用final修饰的静态变量时,什么都不会触
10.java线程的阻塞状态和等待状态
阻塞状态(Blocked) 满足条件但是没有获得锁
线程在获取锁失败时(因为锁被其它线程抢占),它会被加入锁的同步阻塞队列,然后线程进入阻塞状态(Blocked)。处于阻塞状态(Blocked)的线程放弃CPU使用权,暂时停止运行。待其它线程释放锁之后,阻塞状态(Blocked)的线程将在次参与锁的竞争,如果竞争锁成功,线程将进入就绪状态(Runnable) 。
等待状态(WAITING) 由于不满足某种条件而处于等待条件满足的状态,当条件满足的时候,会发出通知,唤醒那些处于等待状态的线程去获锁。若没有获得到,则进入阻塞态
或者叫条件等待状态,当线程的运行条件不满足时,通过锁的条件等待机制(调用锁对象的wait()或显示锁条件对象的await()方法)让线程进入等待状态(WAITING)。处于等待状态的线程将不会被cpu执行,除非线程的运行条件得到满足后,其可被其他线程唤醒,进入阻塞状态(Blocked)。调用不带超时的Thread.join()方法也会进入等待状态。
在操作系统中线程只有5种状态
新建态、就绪态、运行态、阻塞态、死亡态
11.如何重写一个对象的hashCode()方法和equals()方法
12.如何实现一个无锁队列
首先初始化一个队列
初始化一个队列的代码很简,初始化一个dummy结点(注:在链表操作中,使用一个dummy结点,可以少掉很多边界条件的判断),如下所示:
InitQueue(Q)
{
node = new node()
node->next = NULL;
Q->head = Q->tail = node;
}
我们先来看一下进队列用CAS实现的方式,基本上来说就是链表的两步操作:
第一步,把tail指针的next指向要加入的结点。 tail->next = p;
第二步,把tail指针移到队尾。 tail = p;
1. EnQueue(Q, data) //进队列
2. {
3. //准备新加入的结点数据
4. n = new node();
5. n->value = data;
6. n->next = NULL;
7. do {
8. p = Q->tail; //取链表尾指针的快照
9. } while( CAS(p->next, NULL, n) != TRUE);
10. //while条件注释:如果没有把结点链在尾指针上,再试
11. CAS(Q->tail, p, n); //置尾结点 tail = n;
12. }
CAS(p->next, NULL, n) != TRUE 判断p->next是不是为空(p现在是Q->tail),若为空则将p->next指向新建的节点(n),代表将新建节点挂在链表上。While代表一直尝试。挂成功过后,再用p移动到新挂的节点处
这里有一个潜在的问题——如果T1线程在用CAS更新tail指针的之前,线程停掉或是挂掉了,那么其它线程就进入死循环了。下面是改良版的EnQueue()
EnQueue(Q, data) //进队列改良版 v1
{
n = new node();
n->value = data;
n->next = NULL;
p = Q->tail;
oldp = p
do {
while (p->next != NULL)
p = p->next;
} while( CAS(p.next, NULL, n) != TRUE); //如果没有把结点链在尾上,再试
CAS(Q->tail, oldp, n); //置尾结点
}
我们让每个线程,自己fetch 指针 p 到链表尾。但是这样的fetch会很影响性能。而且,如果一个线程不断的EnQueue,会导致所有的其它线程都去 fetch 他们的 p 指针到队尾,能不能不要所有的线程都干同一个事?这样可以节省整体的时间?
比如:直接 fetch Q->tail 到队尾?因为,所有的线程都共享着 Q->tail,所以,一旦有人动了它后,相当于其它的线程也跟着动了,于是,我们的代码可以改进成如下的实现:
EnQueue(Q, data) //进队列改良版 v2
{
n = new node();
n->value = data;
n->next = NULL;
while(TRUE) {
//先取一下尾指针和尾指针的next
tail = Q->tail;
next = tail->next;
//如果尾指针已经被移动了,则重新开始
if ( tail != Q->tail ) continue;
//如果尾指针的 next 不为NULL,则 fetch 全局尾指针到next
if ( next != NULL ) {
CAS(Q->tail, tail, next);
continue;
}
//如果加入结点成功,则退出
if ( CAS(tail->next, next, n) == TRUE ) break;
}
CAS(Q->tail, tail, n); //置尾结点
}
这也是 Java 中的 ConcurrentLinkedQueue 的实现逻辑
13.Java中ConcurrentLinkedQueue中的offer()和poll()方法
无锁队列
public boolean offer(E e) {
checkNotNull(e);
//创建一个新的入队节点 新建节点
final Node<E> newNode = new Node<E>(e);
//创建一个指向尾节点的引用t,相当于一个中间变量,用于和p比较的
//p表示尾节点
//死循环,一直进行CAS操作的尝试,值至成功,返回true
for (Node<E> t = tail, p = t;;) {
//获取p节点的下一节点
Node<E> q = p.next;
//如果q为空,则p为尾节点
if (q == null) {
//p为尾节点,将新节点添加到p节点之后,CAS操作:比较并交换,如果nextOffset的值等于 null的话,说明别的线程还未对其进行修改,则此次CAS操作成功,则跳出循环
if (p.casNext(null, newNode)) {
//如果p不等于t,相当于tail不是尾节点,则通过CAS操作将新节点设置为尾节点,如果失败 了,则说明其他线程已将其进行了修改
if (p != t)
//如果tailOffset的值为t,则尝试将新节点设置为尾节点
casTail(t, newNode);
return true;
}
}
//如果,p等于q,说明p和q都为空,即ConcurrentLinkedQueue刚刚初始化
else if (p == q)
p = (t != (t = tail)) ? t : head;
//p有next节点,表示p的next节点是尾节点,则需要重新更新p后将它指向next节点
else
p = (p != t && t != (t = tail)) ? t : q;
}
}
首先获取尾节点。
然后判断尾节点的next节点是否为空。
如果为空,就尝试使用CAS操作添加节点
如果成功,返回true。
如果失败,表示其他线程已经添加了新节点,则需要重新获取尾节点。
如果不为空,表明其他线程已经添加了新节点进来。
刚初始化。
更新尾节点。
通过分析可知,每次入队时,都会先定位尾节点,定位成功后,通过p.casNext(null, newNode)这个CAS操作来进行入队操作,因为p和t不是每次都相同,即不是每次入队都会重新设置tail节点,这样减少了casTail(t, newNode)这个设置尾节点的CAS操作的数量,减少了开销,提高了入队的效率。
public E poll() {
//死循环,进行出队列操作,直到成功,才返回,要么返回一个item,要么返回null。
restartFromHead:
for (;;) {
//创建一个指向头节点的引用h,相当于一个中间变量,用于和p比较的
//p表示头节点
for (Node<E> h = head, p = h, q;;) {
//item为头节点的元素
E item = p.item;
//如果p节点元素不为空,则使用CAS操作:如果当前线程的item地址的值等于itemOffset地址的 值,就将p节点的元素值设置为空,并返回item
if (item != null && p.casItem(item, null)) {
//如果p不等于h,即head不是头节点,将p的next节点设置为头节点
if (p != h)
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
//表明已是最后一个节点,跳出循环
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
//p与q相同,q和p都为空,跳到外层循环,重新开始
else if (p == q)
continue restartFromHead;
//p有next节点,设置p指向它的next节点
else
p = q;
}
}
}
首先获取头节点的元素。
然后判断头节点元素是否为空。
如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走。
如果非空,则使用CAS操作将头节点的引用设置成null。
如果CAS成功,则直接返回头节点的元素。
如果不成功,表示另外一个线程已经进行了一次出队操作更新了head节点,导致元素发生了变化,需要重新获取头节点。
14 Java中@Value详解
@Value数据来源
spring中有个类org.springframework.core.env.PropertySource
可以将其理解为一个配置源,里面包含了key->value的配置信息,可以通过这个类中提供的方法获取key对应的value信息。
内部有个方法:public abstract Object getProperty(String name); 通过name获取对应的配置信息。
系统有个比较重要的接口:org.springframework.core.env.Environment
其中有:
String resolvePlaceholders(String text);
MutablePropertySources getPropertySources();
resolvePlaceholders用来解析${text}的,@Value注解最后就是调用这个方法来解析的
Spring容器中会有一个Environment对象,最后会调用这个对象的resolvePlaceholders方法解析@Value。
解析@Value的过程:
1. 将@Value注解的value参数值作为Environment.resolvePlaceholders方法参数进行解析
2. Environment内部会访问MutablePropertySources来解析
3. MutablePropertySources内部有多个PropertySource,此时会遍历PropertySource列表,调用PropertySource.getProperty方法来解析key对应的值。
public interface Environment extends PropertyResolver {
String[] getActiveProfiles();
String[] getDefaultProfiles();
/** @deprecated */
@Deprecated
boolean acceptsProfiles(String... var1);
boolean acceptsProfiles(Profiles var1);
}
public interface PropertyResolver {
….
String resolvePlaceholders(String var1);
….
}
15 ConcurrentHashMap中size()方法原理
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
inal long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
sumCount有两个重要的属性baseCount和counterCells,如果counterCells不为空,
那么总共的大小就是baseCount与遍历counterCells的value值累加获得的。
而baseCount是一个volatile变量,每次调用put()方法最后会调用addCount(1L, binCount)方法,
在addCount()中会使用CAS更新baseCount。
在并发情况下,如果CAS修改baseCount失败后,就会使用到CounterCell类,会创建一个对象,
通常对象的volatile的value属性是1。
并发时,利用CAS修改baseCount失败后,会利用CAS操作修改CountCell的值。
16 String中的intern()方法
Jdk1.7之后JVM里字符串常量池放入堆中,之前是放在方法区
intern()方法设计的初衷,就是重用 String 对象,以节省内存消耗
一定是 new 得到的字符串才会调用 intern,字符串常量池中的字符串没必要去intern。
当调用intern方法时,如果池中包含了一个等于此String对象的字符串(比较的是字符串的内容),则返回池中的字符串。否则,常量池中直接存储堆中该字符串
的引用(1.7 之前是常量池中再保存一份该字符串)。
17 Java中interrupted()和isInterrupted()的区别
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
由上面可以看出,interrupted是一个Thread的一个静态方法,里面调用的是当前线程实例对象的isInterrupted()方法,里面参数为true代表会清楚当前的中断标志。
public boolean isInterrupted() {
return isInterrupted(false);
}
isInterrupted()是线程实例的方法,里面调用的是native方法isInterrupted(),false代表不会清楚当前的中断标志。
private native boolean isInterrupted(boolean ClearInterrupted);
18 线程池四大拒绝策略的应用场景
(1) AbortPolicy
这是线程池的默认策略,在任务不能提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
(2) DiscardPoliy
丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。建议一些无关紧要的业务采用此策略。
(3) DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务
是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量
(4) CallerRunsPolicy
将任务回退给调用者,如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务
19 Java中的锁池和等待池(waitSet)
在Java中,每个对象都有两个池,锁(monitor)池和等待池
锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.
深入理解:
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
20 线程中为何stop()和suspend()方法不推荐使用
线程中stop()方法作为一种粗暴的线程终止行为,在线程终止之前没有对其做任何的清除操作,因此具有固有的不安全性。 用Thread.stop()方法来终止线程将会释放该线程对象已经锁定的所有监视器。如果以前受这些监视器保护的任何对象都处于不连贯状态,那么损坏的对象对其他线程可见,这有可能导致不安全的操作。 由于上述原因,因此不应该使用stop()方法,而应该在自己的Thread类中置入一个标志,用于控制目标线程是活动还是停止。如果该标志指示它要停止运行,可使其结束run()方法。如果目标线程等待很长时间,则应使用interrupt()方法来中断该等待。
suspend()方法 该方法已经遭到反对,因为它具有固有的死锁倾向。调用suspend()方法的时候,目标线程会停下来。如果目标线程挂起时在保护关键系统资源的监视器上保持有锁,则在目标线程重新开始以前,其他线程都不能访问该资源。除非被挂起的线程恢复运行。对任何其他线程来说,如果想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。
21 一致性Hash算法中的环偏斜以及”虚拟节点”
22 Reactor模式实现一个高并发的线程池
23 ThreadLocal变量怎么跨线程池使用?
使用InheritableThreadLocal,最后可以用TransmittableThreadLocal来解决
https://www.jianshu.com/p/7a7a4b05a03c
https://www.jianshu.com/p/25857f3bf960
24 使用SignalHandler来处理Linux信号量,控制程序结束的步骤
对于kill -9 pid这个命令,发出的信号是SIGKILL,程序对这个信号是做不了任何处理的。因为这个信号是暴力的终止pid进程,这样的情况下程序就会突然终止,无法做一些保存工作。
而kill pid这个命令是发出SIGTREM信号,java程序对于这个信号是可以捕获到的。因此捕获到这个信号之后,在程序中可以做一些操作来优雅的结束程序。