Java面试题

如有问题感谢大家为我指出

1. Java集合

  • set是无序的,不能有重复的元素(用对象的equals()方法来区分元素是否重复),list是有序的,可以有重复的元素,是线程不安全的,map是key-value的映射,映射关系可以是一对一或多对一,key不可以重复,通过指定的key可以取出value。Set和Map容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果。

2.TreeSet和HashSet区别

HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的,只不过Set用的只是Map的key
HashSet是哈希表实现的,TreeSet是二叉树实现的
HashSet是无序的,TreeSet是有序的(自动排好的)
HashSet可以放入空值null,但是只能放一个,TreeSet不可以放入空值

hashCode和equal()是HashMap用的, 因为无需排序所以只需要关注定位和唯一性即可.
	a. hashCode是用来计算hash值的,hash值是用来确定hash表索引的.
	b. hash表中的一个索引处存放的是一张链表, 所以还要通过equal方法循环比较链上的每一个对象
	才可以真正定位到键值对应的Entry.
	c. put时,如果hash表中没定位到,就在链表前加一个Entry,如果定位到了,则更换Entry中的value,并返回旧value
由于TreeMap需要排序,所以需要一个Comparator为键值进行大小比较.当然也是用Comparator定位的.
	a. Comparator可以在创建TreeMap时指定
	b. 如果创建时没有确定,那么就会使用key.compareTo()方法,这就要求key必须实现Comparable接口.
	c. TreeMap是使用Tree数据结构实现的,所以使用compare接口就可以完成定位了.

2.1红黑树

红黑树的性质:
	1.每个节点非黑即红
	2.根节点是黑色的
	3.空的叶节点都是黑色的
	4.如果一个节点是红色的,那么它的父节点和子节点就只能是黑色的,也就是不能有连续的红色节点。
	5.任意节点到它后代的叶节点的所有路径上,拥有相同数量的黑色节点。

红黑树是特殊的AVL树,遵循红定理和黑定理
红定理:不能有两个相连的红节点
黑定理:根节点必须是黑节点,而且所有节点通向NULL的路径上,所经过的黑节点的个数必须相等

树本身是一种很有效率的数据结构,最简单的有二叉树。
为了满足数据的快速查找,有了查找树或叫排序树。
但是查找树可能会出现某个子树很深,而另外的一些子树很浅,这在查找时会很费时,因此又有了平衡树。
红黑树是平衡树的一种,它的优点在于插入、删除、查找的时间复杂度都是树的高度(lgn),也就是说平均下来,这种数据结构用时更少 

3. HashMap和ConcurrentHashMap

区别:

  • Java1.7 HashMap:
    HashMap是线程不安全的,底层数组+链表实现
    key可以为空,但只能有一个,value可以为空,不限制
    初始容量为16,扩容方法newSize=oldSize*2,size必须是2的n次幂
    先判断是否该扩容在插入:多线程扩容有可能会形成环形链表
    当Map中的元素总数超过数组75%的时候,触发扩容操作,为了减少链表长度,分配更均匀 计算位置的方法
    index=hash&(tab.length-1)

    多线程扩容

// 1.8没有这个方法
static int indexFor(int h, int length) {
     
	return h & (length-1);  
}
  • Java1.8 HashMap:
    HashMap是线程不安全的,底层数组+链表/红黑树实现
    key可以为空,但只能有一个,value可以为空,不限制
    初始容量为16,扩容方法newSize=oldSize*2,size必须是2的n次幂
    先插入在判断是否该扩容:扩容针对整个Map,每次扩容时,原数组中的元素全部依次重新计算位置,重新插入
    插入元素之后才判断是否应该扩容,可能出现无效扩容(扩容之后不在插入数据)
    当Map中的元素总数超过数组75%的时候,触发扩容操作,为了减少链表长度,分配更均匀 计算位置的方法
    index=hash&(tab.length-1)
    1.8HashMap源码
  • Java1.7 ConcurrentHashMap:
    ConcurrentHashMap是线程安全的,底层数组+链表实现
    通过把整个Map分成N个Segment,可提供相同的线程安全,效率提升N倍(默认16倍),读操作不加锁,因HashEntry的value变量是volatile的,也能保证读取到最新的值 ConcurrentHashMap允许多个修改操作并发执行,是因为使用了锁分离技术有些地方需要跨段,比如size()和containsValue(),它们可能需要锁住整个表而不是某个段,这需要按顺序锁住所有段,操作结束后,按顺序释放所有段的锁
    扩容不针对整个Map,段内扩容,段内元素超过该段对应Entry数组长度75%的时候扩容,插入前检查是否扩容,有效避免无效扩容
  • Java1.8 ConcurrentHashMap:
    整体结构:移除 Segment,使锁的粒度更小,Synchronized + CAS + Node + Unsafe
    put():由于移除了 Segment,类似 HashMap,可以直接定位到桶,拿到 first 节点后进行判断:①为空则 CAS 插入;②为 -1 则说明在扩容,则跟着一起扩容;③ else 则加锁 put(类似1.7)
    get():基本类似,由于 value 声明为 volatile,保证了修改的可见性,因此不需要加锁
    resize():支持并发扩容,HashMap 扩容在1.8中由头插改为尾插(为了避免死循环问题),ConcurrentHashmap 也是,迁移也是从尾部开始,扩容前在桶的头部放置一个 hash 值为 -1 的节点,这样别的线程访问时就能判断是否该桶已经被其他线程处理过了
    size():用 baseCount 来存储当前的节点个数,这就设计到 baseCount 并发环境下修改的问题
    HashTable:
    HashTable是线程安全的,实现线程安全的方法是锁住整个HashTable,效率低,底层数组+链表实现,key和value都不能为null
    初始值11,扩容方法newSize=oldSize*2+1
    位置计算方法 index=(hash & 0x7FFFFFFF) % tab.length

https://www.cnblogs.com/heyonggang/p/9112731.html
https://baijiahao.baidu.com/s?id=1660315139062856790&wfr=spider&for=pc

4. 线程池参数

4.1线程池的优势

  1. 降低系统资源消耗,通过复用已创建的线程,降低线程创建和销毁造成的消耗
  2. 提高系统响应速度,当有任务到达时,可以通过复用已创建的线程迅速响应,无需等待新线程的创建便能立即执行
  3. 方便线程并发数的管控。若是线程无限创建,可能会导致内存占用过多而产生OOM,并且会造成CPU过度切换(CPU切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))
  4. 提供更强大的功能,延时定时线程池

4.2线程池的主要参数

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  1. corePoolSize(核心线程数):向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也创建一个新的线程执行,直到创建的线程数大于或者等于corePoolSize(除了利用提交新任务来创建和启动线程,也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
  2. maximumPoolSize(最大线程数):线程池所允许的最大线程数量。当队列满了,且已创建的线程数小于maximumPoolSize,线程池会创建新的线程来执行;对于无界队列,可忽略此参数。
  3. keepAliveTime(线程存活时间):当线程池中的线程数大于核心线程数时,线程的空闲时间超过keepAliveTime就会被销毁,直到线程池中的数量小于核心线程数
  4. unit(时间单位):存活时间的单位
  5. workQueue(线程队列):用于传输和保存等待执行任务的阻塞队列

workQueue可用的队列类型

workQueue的类型是BlockingQueue<Runnable>,通常可以取下面三种类型:
1. 有界队列ArrayBlockingQueue:基于数组的先进先出队列,创建时必须规定大小
2. 无界任务队列LinkedBlockingQueue:基于链表的先进先出队列,如创建时未指定大小,则默认Integer.MAX_VALUE
3. 直接提交队列synchronousQueue:这个队列比较特殊,他不会保存提交的任务,而是将直接新创建一个线程来执行新来的任务
  1. ThreadFactory threadFactory(线程工厂):用于创建新的线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)
  2. handler(表示当拒绝处理任务时的策略):当线程池和队列都满的时候,再加入线程就会执行此策略

拒绝策略

1. AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException
这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
2. DiscardPolicy:丢弃任务,但不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃
使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。例如,本人的博客网站统计阅读量就是采用的这种拒绝策略。
3. DiscardOldestPolicy:丢弃线程队列中最前面的任务,重新提交被拒绝的任务
此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
4. CallerRunsPolicy:由调用线程处理该任务
只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。

4.3线程池流程

线程池主要处理过程

  1. 判断核心线程池是否已经满了,否:创建线程池执行任务;是:
  2. 判断队列是否已经满了,否:放入队列,等待执行;是:
  3. 判断线程池是否已经满了,否:创建新的线程执行任务;是:执行饱和策略

4.4线程池为什么需要使用(阻塞)队列?

  1. 若是无限创建线程,可能会导致内存占用过多导致OOM,并且会造成CPU过渡切换。
  2. 创建线程池消耗高(线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲)

4.5线程池为什么使用阻塞队列而不使用非阻塞队列?

阻塞队列可以保证在没有任务的时候阻塞获取任务的线程,使线程进入wait状态,从而释放CPU资源。
当队列中有任务时才唤醒线程从队列中拿出任务执行。
使得线程不至于一直占用CPU资源。
线程执行完通过循环继续执行下一个任务,代码如下

while (task != null || (task = getTask()) != null)

4.6如何配置线程池

CPU密集型任务
尽量使用较小的线程池,一般是CPU核心数+1,因为CPU密集型任务使CPU的使用率高,若开过多线程,会导致CPU过渡切换

IO密集型任务
可使用较大的线程池,一般为2*CPU核心数。IO密集型任务对CPU的使用率不高,因此可以让CPU等待IO任务执行的时候,分出线程去执行其他任务,充分利用CPU时间

混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。
因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失

4.7Java中提供的线程池

Executors类提供了四种不同的线程池:CachedThreadPool、FixedThreadPool、ScheduledThreadPool、SingleThreadExecutor
四种线程池比较

  1. CachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务执行时间短,可以很快结束,不会造成CPU过渡切换)
  2. FixedThreadPool:创建一个固定大小的线程池,因使用无界的阻塞队列,所以实际的线程数永远不会变化,适用于负载较重的场景,对线程数进行限制(保证线程数可控,不会造成线程数过多,导致系统负载更为严重)
  3. SingleThreadExecutor:创建一个单线程的线程池,适用于保证任务的顺序执行
  4. ScheduledThreadPool:适用于执行延时或周期性任务

4.8execute()和submit()方法

  1. execute(),执行一个任务,无返回值
  2. submit(),提交一个任务,有返回值
    submit(Callable task)能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)
    submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。
    submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。

Future.get方法会使取结果的线程进入阻塞状态,知道线程执行完成之后,唤醒取结果的线程,然后返回结果。

https://www.jianshu.com/p/7726c70cdc40
https://blog.csdn.net/suifeng629/article/details/98884972

5.锁

5.1CAS机制

CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

  1. 在内存地址V当中,存储着值为10的变量
    内存地址V,数据10
  2. 此时线程1想把变量的值增加1.对线程1来说,旧的预期值A=10,要修改的新值B=11
    线程1
  3. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11
    线程2更新值为11
  4. 线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败
    A!=V提交失败
  5. 线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋
    自旋
  6. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的
    相等
  7. 线程1进行交换,把地址V的值替换为B,也就是12
    更新值为12

5.1.1CAS的缺点

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值