Java面试题QA

Posted 木子道

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java面试题QA相关的知识,希望对你有一定的参考价值。

1、Q:为什么要重写hashcode()和equals()以及他们之间的区别与关系;    1、Q:为什么要重写hashcode()和equals()以及他们之间的区别与关系;1、Q:为什么要重写hashcode()和equals()以及他们之间的区别与关系;1、Q:为什么要重写hashcode()和equals()以及他们之间的区别与关系;

A:equals()相同,hashcode()必定相同.|equals()不同,hashcode有可能相同,也有不相同

hashcode()相同,equals()不一定相同|hashcode()不同,equals()一定不相同

保证同一个对象在equals相同情况下,hashcode必定相同,提高效率

    2、Q:若hashcode方法永远返回1或者一个常量会产生什么结果?

A:产生hash冲突

    3、Q: Collections和Arrays的sort方法默认的排序方法是什么;

A:Timsort;

    4、Q:引用计数法与GC Root可达性分析法区别

A:对象添加一个引用计数器,引用时值加1,引用失效减1,GCRoot 对象可达性。GC Root 起始点往下找,当对象没有到GC Root的引用链,则表示不可达, 引用计数快,但不准确。

    5、Q:HashSet方法里面的hashcode存在哪,如果重写equals不重写hashcode会怎么样?

A:Map,HashSet底层实现是map,会产生不相关的两个对象会相同。

    6、Q:反射的作用和原理?

A:运行状态中,可以任意对类、方法、对象实现动态调用的功能称为反射。原理:通过字节码找到这个类。

    7、Object类中常见的方法,为什么wait  notify会放在Object里边?

因为wait和notify出现在synchronized代码块中,在synchronized锁是可以任意对象,所以任意对象都可以调用wait()和notify()

    8、JAVA8特性

1.Lambda表达式

2.Stream函数式操作流元素集合

3.接口新增:默认方法与静态方法

4.方法引用,与Lambda表达式联合使用

5.引入重复注解

6.类型注解

7.最新的Date/Time API (JSR 310)

8.新增base64加解密API

9.数组并行(parallel)操作

10.JVM的PermGen空间被移除:取代它的是Metaspace(JEP 122)元空间

    9、HashMap 和 ConcurrentHashMap 的区别

HashMap本质是数组加链表,根据key获取hash值,然后计算出数组下标,如果多个key对应到用一个下标,就会链表串起来,线程不安全

CoucurrentHashMap将数据分为多个segment分段锁,默认是16个锁,然后每次操作对一个segment加锁,避免多线程锁的几率,提高并发效率;线程安全

    10、HashMap 的工作原理及代码实现,什么时候用到红黑树

通过hash的方法,通过put和get存储和获取对象,存储对象时,put方法传入k/v,首先计算hashcode的值得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量2倍,获取对象时,get方法传入k,调用hashcode得到bucket位置,并进一步调用equals方法确定键值对,如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在java8中,如果bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度。

    11、多线程情况下HashMap死循环的问题

HashMap采用链表解决Hash冲突,因为是链表结构,那么就很容易形成闭合的链路,这样在循环的时候只要有线程对这个HashMap进行get操作就会产生死循环。只有在多线程并发的情况下才会出现这种情况,那就是在put操作的时候,如果size>initialCapacity*loadFactor,那么这时候HashMap就会进行rehash操作,随之HashMap的结构就会发生翻天覆地的变化。很有可能就是在两个线程在这个时候同时触发了rehash操作,产生了闭合的回路。

    12、HashMap出现Hash DOS攻击的问题

无论我们服务端使用什么语言,我们拿到json格式的数据之后都需要做jsonDecode(),将json串转换为json对象,而对象默认会存储于Hash Table,而Hash Table很容易被碰撞攻击。我只要将攻击数据放在json中,服务端程序在做jsonDecode()时必定中招,中招后CPU会立刻飙升至100%。16核的CPU,16个请求就能达到DoS的目的。

如何防御:首先我们需要增加权限验证,最大可能的在jsonDecode()之前把非法用户拒绝。其次在jsonDecode()之前做数据大小与参数白名单验证

    13、ConcurrentHashMap 的工作原理,如何统计所有的元素个数

  HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁。那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。  另外,ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁。

   ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put , remove和clear方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。size()的实现还有一点需要注意,必须要先segments[i].count,才能segments[i].modCount,这是因为segment[i].count是对volatile变量的访问,接下来segments[i].modCount才能得到几乎最新的值,这里和get方法的方式是一样的,也是一个volatile写 happens-before volatile读的问题。

14、ThreadLocal为什么会出现OOM,出现的深层次原理    14、ThreadLocal为什么会出现OOM,出现的深层次原理14、ThreadLocal为什么会出现OOM,出现的深层次原理

hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

ThreadLocal里面使用了一个存在弱引用的map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。

但是,我们的value却不能回收,而这块value永远不会被访问到了,所以存在着内存泄露。因为存在一条从current thread连接过来的强引用。只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收。最好的做法是将调用threadlocal的remove方法,这也是等会后边要说的。

在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

15、线程池原理    15、线程池原理15、线程池原理

线程池的优点:

  • 重用线程池中的线程,减少因对象创建,销毁所带来的性能开销;

  • 能有效的控制线程的最大并发数,提高系统资源利用率,同时避免过多的资源竞争,避免堵塞;

  • 能够多线程进行简单的管理,使线程的使用简单、高效。

16、线程池的几种实现方式    16、线程池的几种实现方式16、线程池的几种实现方式

初始化4种类型的线程池:

newFixedThreadPool()说明:初始化一个指定线程数的线程池,其中corePoolSize == maxiPoolSize,使用LinkedBlockingQuene作为阻塞队列特点:即使当线程池没有可执行任务时,也不会释放线程。newCachedThreadPool()说明:初始化一个可以缓存线程的线程池,默认缓存60s,线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;特点:在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源;当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销;因此,使用时要注意控制并发的任务数,防止因创建大量的线程导致而降低性能。newSingleThreadExecutor()说明:初始化只有一个线程的线程池,内部使用LinkedBlockingQueue作为阻塞队列。特点:如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行newScheduledThreadPool()特定:初始化的线程池可以在指定的时间内周期性的执行所提交的任务,在实际的业务场景中可以使用该线程池定期的同步数据。

总结:除了newScheduledThreadPool的内部实现特殊一点之外,其它线程池内部都是基于ThreadPoolExecutor类(Executor的子类)实现的。

  • corePoolSize:核心线程数

    • 核心线程会一直存活,及时没有任务需要执行

    • 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理

    • 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭

  • queueCapacity:任务队列容量(阻塞队列)

    • 当核心线程数达到最大时,新任务会放在队列中排队等待执行

  • maxPoolSize:最大线程数

    • 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务

    • 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常

  • keepAliveTime:线程空闲时间

    • 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize

    • 如果allowCoreThreadTimeout=true,则会直到线程数量=0

  • allowCoreThreadTimeout:允许核心线程超时

  • rejectedExecutionHandler:任务拒绝处理器

    • AbortPolicy 丢弃任务,抛运行时异常

    • CallerRunsPolicy 执行任务

    • DiscardPolicy 忽视,什么都不会发生

    • DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务

    • 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务

    • 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务

    • 两种情况会拒绝处理任务:

    • 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常

    • ThreadPoolExecutor类有几个内部实现类来处理这类情况:

    • 实现RejectedExecutionHandler接口,可自定义处理器

17、线程的生命周期,状态是如何转移的    17、线程的生命周期,状态是如何转移的17、线程的生命周期,状态是如何转移的

1、新建状态用new Thread()建立一个线程对象后,该线程对象就处于新生状态。

2、就绪状态通过调用线程的start方法进入就绪状态(runnable)。注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(就绪池),等待系统为其分配CPU。Note:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。

3、运行状态处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态,重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。

4、阻塞状态当发生如下情况时,线程会让出CPU控制权并暂时停止自己的运行,从运行状态变为阻塞状态:① 线程调用sleep方法主动释放CPU控制权② 线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞③ 线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有④ 线程在等待某个通知(notify)⑤ 程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。当发生如下情况时,线程会从运行状态变为阻塞状态:

5、死亡状态当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。线程一旦死亡,就不能复生。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

18、什么是线程安全,如何保证线程安全   18、什么是线程安全,如何保证线程安全18、什么是线程安全,如何保证线程安全18、什么是线程安全,如何保证线程安全111111111111231231231123123

线程安全就是: 在多线程环境中,能永远保证程序的正确性。 就是多线程访问同一代码,不会产生不确定结果。(比如死锁)

只有存在共享数据时才需要考虑线程安全问题。

如何保证呢:

第一种,修改线程模型。即不在线程之间共享该状态变量。一般这个改动比较大,需要量力而行。

第二种,将对象变为不可变对象。有时候实现不了。

第三种,就比较通用了,在访问状态变量时使用同步。 synchronized和Lock都可以实现同步。简单点说,就是在你修改或访问可变状态时加锁,独占对象,让其他线程进不来。

这也算是一种线程隔离的办法。(这种方式也有不少缺点,比如说死锁,性能问题等等)

其实有一种更好的办法,就是设计线程安全类。《代码大全》就有提过,问题解决得越早,花费的代价就越小。

是的,在设计时,就考虑线程安全问题会容易的多。

首先考虑该类是否会存在于多线程环境。如果不是,则不考虑线程安全。

然后考虑该类是否能设计为不可变对象,或者事实不可变对象。如果是,则不考虑线程安全

最后,根据流程来设计线程安全类。

设计线程安全类流程:

1、找出构成对象状态的所有变量。

2、找出约束状态变量的不变性条件。

3、建立对象状态的并发访问管理策略。


以上是关于Java面试题QA的主要内容,如果未能解决你的问题,请参考以下文章

Java进阶之光!2021必看-Java高级面试题总结

经验总结:Java高级工程师面试题-字节跳动,成功跳槽阿里!

面试题QA

前端面试题之手写promise

一道经典面试题:字符串在Java中如何通过“引用”传递

Java之String相关内容详解(字符串和字符串常量池)面试题