最近在准备有赞的面试,看了下相关面经,感觉都是地狱级别的.......... 心都凉了,还是等春招吧.
题目来自牛客, 题的答案都是我个人理解的或网上的参考,仅仅只作为复习用.因个人水平有限,不合理的地方请多多指正.
1.HashMap链表转红黑树后会不会再转会链表,转链表的阈值是多少,为什么要设置为这个值?
HashMap在jdk1.8之后引入了红黑树的概念,表示若桶中链表元素超过8时,会自动转化成红黑树;若桶中元素小于等于6时,树结构还原成链表形式。
原因:
红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。
还有选择6和8的原因是:
中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
2.HashMap扩容后下标可能的取值
3.HashSet如何保证元素的唯一性?
当HashSet调用add()方法存储对象的时候, 先调用对象的hashCode()方法得到一个哈希值, 然后在集合中查找是否有哈希值相同的对象
如果没有哈希值相同的对象就直接存入集合
如果有哈希值相同的对象, 就和哈希值相同的对象逐个进行equals()比较,比较结果为false就存入, true则不存
4.wait()和sleep()的区别
- wait()来自Object类,sleep()来自Thread类
- 调用 sleep()方法,线程不会释放对象锁。而调用 wait() 方法线程会释放对象锁;
- sleep()睡眠后不出让系统资源,wait()让其他线程可以占用 CPU;
- sleep(millionseconds)需要指定一个睡眠时间,时间一到会自然唤醒。而wait()需要配合notify()或者notifyAll()使用
5.Spring的Controller是单例的,那它是线程安全的吗?如果现在有十个线程去对其中一个静态变量做自增操作,会出问题吗?
controller默认是单例的,不要使用非静态的成员变量(service无所谓,因为它不会变),否则会发生数据逻辑混乱。比如a线程将int i=3,b线程将 i = 4,然后a再访问 i 时, i的值为4
正因为单例所以不是线程安全的。
解决方案:
有几种解决方法:
1、在Controller中使用ThreadLocal变量
2、在spring配置文件Controller中声明 scope="prototype",每次都创建新的controller
所在在使用spring开发web 时要注意,默认Controller、Dao、Service都是单例的。
6.如何保证它线程安全?使用锁的话Synchronized和Lock有什么区别?
1: Lock是java的一个interface接口,而synchronized是Java中的关键字,synchronized是由JDK实现的,不需要程序员编写代码去控制加锁和释放;
2、synchronized修饰的代码在执行异常时,jdk会自动释放线程占有的锁,不需要程序员去控制释放锁,因此不会导致死锁现象发生;但是,当Lock发生异常时,如果程序没有通过unLock()去释放锁,则很可能造成死锁现象,因此Lock一般都是在finally块中释放锁;
3、synchronized是非公平锁,Lock可以设置是否公平锁,默认是非公平锁;
4、Lock锁的范围有局限性,仅适用于代码块范围,而synchronized可以锁住代码块、对象实例、类;
7.SQL里update语句加的什么锁?
InnoDB行锁是通过给索引上的索引项加锁来实现的。
InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。
如果没有索引,所以update会锁表,如果加了索引,就会锁行
8.如果update后面where条件是个唯一索引的字段,是加的表锁还是行锁?
InnoDB行锁是通过给索引上的索引项加锁来实现的。
InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。
加的是行锁
9.如果是普通索引呢?加的什么锁?
普通索引加的也是行锁
10.锁可以在事务外使用吗?同一个事务里的锁可以重入吗?
不一定啊。首先是选择什么样的存储引擎,这个关系很大。比较Innodb就支持行锁,因为有MVCC可以保证读写不冲突。
11.还是那个自增问题,放在Redis里会存在线程安全问题吗?
-
redis是单线程运行,所以多个redis命令是一个一个执行,所以是线程安全的
-
但是分开的两个redis命令,对于整个应用来说不是线程安全的,因为这两个redis命令之间会有其他命令,就像多线程环境下,java线程不安全的i++操作,这个两个redis命令没有事务管理
-
可以用RPOPLPUSH或者INCR , 或者lua脚本,实现多个redis操作合为一个命令,这样就对于线程安全了
12.Spring框架源码看过吧,了解哪些,讲一下IOC的源码
这个问题太大了, 不会
13.循环依赖怎么解决,二级缓存行不行?
1 什么是循环依赖? what?
(1)循环依赖-->循环引用。--->即2个或以上bean 互相持有对方,最终形成闭环。
2 Spring中循环依赖的场景?where?
①:构造器的循环依赖。【这个Spring解决不了】
因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决
②【setter循环依赖】field属性的循环依赖【setter方式 单例,默认方式-->通过递归方法找出当前Bean所依赖的Bean,然后提前缓存【会放入Cach中】起来。通过提前暴露 -->暴露一个exposedObject用于返回提前暴露的Bean。】
3. 如何检测是否有循环依赖?how to find?
可以 Bean在创建的时候给其打个标记,如果递归调用回来发现正在创建中的话--->即可说明循环依赖。
4.怎么解决的? todo what?
Spring的循环依赖的理论依据其实是基于Java的引用传递,当我们获取到对象的引用时,对象的field或zh属性是可以延后设置的(但是构造器必须是在获取引用之前)。
Spring的单例对象的初始化主要分为三步:
①:createBeanInstance:实例化,其实也就是 调用对象的构造方法实例化对象
②:populateBean:填充属性,这一步主要是多bean的依赖属性进行填充
③:initializeBean:调用spring xml中的init() 方法。
从上面讲述的单例bean初始化步骤我们可以知道,循环依赖主要发生在第一、第二步。也就是构造器循环依赖和field循环依赖。
那么我们要解决循环引用也应该从初始化过程着手,对于单例来说,在Spring容器整个生命周期内,有且只有一个对象,所以很容易想到这个对象应该存在Cache中,Spring为了解决单例的循环依赖问题,使用了三级缓存。
这三级缓存分别指:
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); //一级缓存
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 二级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 三级缓存
singletonFactories : 单例对象工厂的cache
earlySingletonObjects :提前暴光的单例对象的Cache 。【用于检测循环引用,与singletonFactories互斥】
singletonObjects:单例对象的cache
将构造函数注入方式改为 属性注入方式 即可
二级缓存不可以解决循坏依赖,二级缓存会导致bean的属性还没注入就使用.
14.ThreadLocal实现原理,为什么会导致内存泄漏?
多线程访问同一个共享变量的时候容易出现并发问题,ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。
因为线程一直不死的话,ThreadLocalMap的key-value一直在涨,会导致泄漏。
15.软引用和弱引用回收实现的底层原理是什么?是在哪个GC被回收的?
1:强引用
Object object=new Object();那object就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
2、软引用(SoftReference)
如果一个对象只具有软引用,如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
3、弱引用(WeakReference)
如果一个对象只具有弱引用。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
4、虚引用(PhantomReference)
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。
16.ConCurrentHashMap里的size()方法是怎么实现的?
ConCurrentHashMap put()的流程 对当前的table进行无条件自循环直到put成功
- 如果没有初始化就先调用initTable()方法来进行初始化过程
- 如果没有hash冲突就直接CAS插入
- 如果还在进行扩容操作就先进行扩容
- 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
- 最后一个如果Hash冲突时会形成Node链表,在链表长度超过8,Node数组超过64时会将链表结构转换为红黑树的结构,break再一次进入循环
- 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容
ConcurrentHashMap的get()操作的流程很简单,也很清晰,可以分为三个步骤来描述
- 计算hash值,定位到该table索引位置,如果是首节点符合就返回
- 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
- 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
ConcurrentHashMap的size()操作
在put操作结束后,会调用addCount,更新 baseCount计数。 baseCount使用volatile修饰.
并发时,会利用CAS操作修改CountCell的值,
先利用sumCount()
计算,然后如果值超过int的最大值,就返回int的最大值。但是有时size就会超过最大值,这时最好用mappingCount
方法
17.AQS具体怎么实现的?
aqs全称为AbstractQueuedSynchronizer,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。
AQS的实现依赖内部的同步队列,也就是FIFO的双向队列,如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
AQS的实现主要是基于非公平锁的独占锁实现。在获得同步锁时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
18.几种常用的线程池使用场景
线程池适合单系统的大量的异步任务处理,比如发送短信、保存日志。
19.Condition具体是怎么实现的?
Condition是AQS的内部类。每个Condition对象都包含一个队列(等待队列)。等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。
调用await方法后,将当前线程加入Condition等待队列中。当前线程释放锁。否则别的线程就无法拿到锁而发生死锁。自旋(while)挂起,不断检测节点是否在同步队列中了,如果是则尝试获取锁,否则挂起。当线程被signal方法唤醒,被唤醒的线程将从await()方法中的while循环中退出来,然后调用acquireQueued()方法竞争同步状态。
20.锁升级过程说一下,偏向锁状态来了其他线程竞争会发生什么变化?
1:无锁
无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。也就是CAS(CAS是基于无锁机制实现的)。
2.偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。
3.轻量级锁
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
4.重量级锁
在轻量级锁状态下,如果有第三个来访时,就会自动升级成重量级锁
21.如何实现一个取号器?你有哪些方法?讲下具体实现
22.如何实现一个延时队列?用到的api都说一下具体怎么实现的?
23.Redis里底层存储结构是怎样的?不是问具体的数据结构有哪些。。
数据类型五种:
- String
- Hash
- List
- Set
- ZSet
redis数据结构:
简单动态字符串 string 采用结构sdshdr和sds封装了字符串
链表 list是一个(双向链表)结构
字典又称为符号表或者关联数组、或映射(map),是一种用于保存键值对的抽象数据结构。
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的
整数集合(intset)是Redis用于保存整数值的集合抽象数据类型,它可以保存类型为int16_t、int32_t 或者int64_t 的整数值,并且保证集合中不会出现重复元素。
压缩列表(ziplist)是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
24.Redis哨兵有什么缺点?
哨兵模式优缺点
优点:
1、哨兵集群,基于主从复制模式,所有的主从配置优点,它都有
2、主从可以切换,故障可以转移,高可用性的系统
3、哨兵模式就是主从模式的升级,手动到自动,更加健壮
缺点:
1、Redis不好在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦
2、哨兵模式的配置繁琐