Java基础部分面试问题

接下里几篇文章,都是我在去年秋招中遇到过的java真实面试题。这里作了一下总结。主要分为Java基础部分、JVM部分、数据结构与算法部分、计算机网络部分、操作系统部分、数据库部分、中间件部分、Spring框架部分以及大数据面试题。其中的问题的回答都是我在面试中的回答,有对有错。写这几篇文章的目的在于和大家多多交流,也希望大家能多多指正问题。

**

Java基础部分

**
1.CAS算法中的ABA问题怎么去解决?
使用AtomicStampedReference,即对象引用类型的原子类。通过其中的版本号增加机制,每改变一次对象就增加版本号的值。在改变对象时,比较当前线程记录下的版本号与对象自己的版本号。如果相同则则允许改变,否则不允许改变。

2.Java中的内部类
Map中的Entry<K,V>、ArrayList中的迭代器都是内部类以及ThreadPoolExecutor中的四大拒绝策略都是内部类。
优点/好处:

  1. 内部类可以访问该类定义所在作用域中的数据,包括private修饰的 -----无条件访问外部类元素
  2. 内部类对同一包内的其他类隐藏 -----实现隐藏,安全性
    使用private或者protected修饰符来修饰内部类,就可以实现对外隐藏。
  3. 内部类+接口可以实现多继承 -----实现多继承
  4. 通过匿名内部类可以优化简单的接口实现
    内部类是有可能引起内存泄漏的问题的
    如果当内部类的引用被外部类以外的其他类引用时,就会造成内部类和外部类无法被GC回收的情况,即使外部类没有被引用,因为内部类持有指向外部类的引用)。

3.Java中对于泛型的理解
为什么使用泛型?

  1. 可以做到代码复用,减少代码的重复
  2. 保证类型的安全,在一个集合中添加类型不同的元素时,会在编译期就提示出错误
  3. 编程的便利
    泛型,即”参数化类型”。怎么理解呢?就是把原本传入方法中参数的具体类型变为一种参数,类似于方法中的变量参数。
    比如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程序对于这个信号是可以捕获到的。因此捕获到这个信号之后,在程序中可以做一些操作来优雅的结束程序。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值