【牛客网面经整理】20200831小米一面

自我介绍

答:能否胜任;性格中的闪光点;
从业时间;教育背景;工作经验;项目经验;与众不同之处;技术;性格;

算法:二叉搜索树中两个子节点的最近祖先节点。二叉树无序情况下如何寻找?

答:递归

说说hashmap底层源码

答:

1、特点(实现接口):

public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
因为实现了这些接口,所以具有这些接口的特点;

  • Map:是存放一对值的最大接口!即接口中的每个元素都是一对, 以key -->
    value的形式保存,并且Key是不重复的,元素的存储位置由key决定。也就是可以通过key去寻找key-value的位置,从而得到value的值。适合做查找工作。
  • Cloneable:Cloneable是标记型的接口,它们内部都没有方法和属性,实现
    Cloneable来表示该对象能被克隆,能使用Object.clone()方法。如果没有实现
    Cloneable的类对象调用clone()就会抛出CloneNotSupportedException。
  • Serializable:public interface Serializable类通过实现 java.io.Serializable
    接口以启用其序列化功能。
  • iterator:可以使用迭代器遍历

2、底层数据结构:

java1.7:数组+链表
java1.8:数组+链表+红黑树
原因:提高性能,解决发送哈希碰撞后,链表过长而导致索引效率慢的问题O(n)–O(logn)
当链表长度>8时,将该链表转换为红黑树(无冲突时:存放在数组;冲突和链表长度大于8时:存放在单链表;冲突和链表长度>8时:存放在红黑树)

java1.7:Entry
java1.8:TreeNode

3、具体使用:

get(Object key);
put(K key,V value);
putAll();
remove(Object key);
containsKey(Object key);
containsValue(Object value);
clear();//清除哈希表中的所有键值对
size();
isEmtry();

4、基础知识:

hashmap中的重要参数(变量)
java1.7:容量、加载因子(0.75)、扩容阈值
java1.8:容量、加载因子(0.75)、扩容阈值、桶的树化阈值(>8)、桶的链表还原阈值(<6)、最小树形化容量阈值(>64)
加载因子核心思想(需要在时间效率和空间利用率之间寻找一种平衡)

5、源码分析:

构造函数4个:

(1)默认构造函数无参
(2)指定容量大小的构造函数
(3)指定容量大小和加载因子的构造函数
(4)包含子map的构造函数

插入操作:

tab是否为空,如果是resize创建————根据键值key计算hash值,计算下标————插入时需要判断是否存在hash冲突————如果不存在直接插入
————如果存在哈希冲突————1、判断当前位置的key和需要插入的key是否相/2、判断当前节点类型是否是红黑树
————如果相同新值覆盖旧值
————如果是红黑树直接在书中插入;如果是链表使用尾插法插入,插入后判断链表节点是否大于数阈值,则将链表转换为红黑树
————插入成功后,判断实际存在的键值对数量size》最大容量,如果大于则扩容
与jdk1.7的区别:
1、初始化方式:1.8直接集成在扩容函数中resize;1.7单独函数inflateTable
2、hash值的计算方法:1.8hashcode,2次扰动处理;1.7hashcode,9次扰动处理
3、存放数据的位置判断:1.8数组红黑树链表;1.7数组链表
4、插入数据方式:1.8尾插;1.7头插

扩容机制:

插入键值对后,发现容量不足————开始扩容————异常情况判断(是否需要初始化,若当前容量大于最大值则不扩容)————根据新容量(2倍)新建数组————保存旧数组————遍历旧数组的每个数据————重新计算每个数据在新数组中的存储位置————将旧数组上的每个数据逐个转移到新数组中————新数组table引用到hashmap的table属性上————重新设置扩容阈值————结束
与jdk1.7的区别:
1、1.8扩容后的位置:原位置or(原位置+旧容量);1.7按照原来的方式计算
2、1.8尾插法:直接插入到链表尾部/红黑树,不会出现逆序&环形链表死循环问题;1.7头插法
3、1.8扩容前插入、转移数据时统一计算;1.7扩容后插入,转入数据时单独计算

查询机制

计算需获取数据的hash值————计算存放在数组table中的位置————依次在数组、红黑树、链表中查找————获取后,判断所获取的数据是否为空————结束

hash值是如何计算的

答:
java1.8 1:hashcode()2:扰动处理=2次扰动=1次位运算+1次异或运算

static final int hash(Object key) {
           int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
            // a. 当key = null时,hash值 = 0,所以HashMap的key 可为null      
            // 注:对比HashTable,HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
            // b. 当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),然后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制
     }

java1.7 1:hashcode()2:扰动处理=9次扰动=4次位运算+5次异或运算

   static final int hash(int h) {
        h ^= k.hashCode(); 
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
     }

总结:步骤
1:计算哈希码
h=key.hashCode();
Object类中提供的方法
作用:根据对象的内存地址,经过特定算法返回一个哈希码
意义:保证每个对象的哈希码尽可能不同,从而提高在散列结构存储中查找的效率
2:二次处理哈希码
h^(h>>>16)
扰动处理
哈希码异或哈希码自身右移16位后的二进制
本质:二次处理低位=哈希码的高16位不变、低16位=低16位异或高16位(高位参与低位的运算)
3:最终计算存储的数组的位置
h&(length-1)二次处理后的哈希码与运算(数组长度-1)

为什么要这么麻烦的计算呢?
根本目的:为了提高存储key-value的数组下标位置的随机性&分布均匀性,尽量避免出现hash值冲突。即对于不同key,存储的数组下标位置尽可能不一样

为什么不直接用hashcode呢?
答:哈希码可能与数组大小范围不匹配。

为什么采用哈希码与运算数组长度-1计算数组下标呢?
答:解决哈希码与数组大小范围不匹配的问题。

为什么数组长度=2的次幂?
答:1、只有当数组长度=2的次幂时,h&(length-1)才等价于h%length;
2、只有当length-1的结果=奇数时,使得哈希码&数组长度-1的结果最后一位可能是1或0(取决于哈希码),保证了存储位置奇偶均匀分布

为什么在计算数组下标前,需要对哈希码进行二次处理:扰动处理?
答:加大哈希码低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少hash冲突

如何解决hash冲突

答:发送hash冲突————判断当前节点的数据结构(红黑树or链表)————(如果是红黑树)在红黑树中插入or更新数据————需插入的数据存放在红黑树中
————(如果是链表)在链表中插入or更新数据————需插入的数据存放在链表中————插入后,判断是否需要执行树化操作————插入后判断是否需要扩容

HashMap为什么线程不安全?

答:1.7 头插法 在并发的情况下,发生扩容时,可能会产生循环链表,在执行get的时候,会触发死循环,引起CPU的100%问题,所以一定要避免在并发情况下使用HashMap
1.8 尾插法上述问题没有但是没有实现同步锁

为什么HashMap中String、Integer这样的包装类适合作为key键?

答:1:String、Integer等包装类的特性,保证了Hash值的不可更改性&计算准确性
2:有效减少了 发生Hash碰撞的几率

原因:1、String、Integer等包装类的特性,final类型即具有不可变性,保证了key的不可更改性,不会出现放入和获取时哈希表不同的情况
2、内部已重写了equals()、hashcode()严格遵守相关规范、不容易出现hash值的计算错误

synchronized和volatile的区别

答:都是JMM根据并发过程中如何处理可见性、原子性和有序性这三个特性而建立的模型。底层就是解决缓存一致性,处理器优化和指令重排序问题。

  • 原子性:是指在一个操作cpu不可以在中途暂停然后再调度,即要不执行完,要不不执行。synchronized可保证
  • 可见性:指当多个线程访问一个变量时,对变量的修改其他线程可见。volatile、final。synchronized可保证
  • 有序性:程序执行的顺序按照代码先后顺序执行。volatile、synchronized可保证

JMM是通过禁止特定类型的编译器重排序和处理器重排序来为程序员提供一致的内存可见性。例如A线程具体什么时候刷新共享数据到主内存是不确定的,假设使用同步原语,那么刷新的时间是确定的。

  • 线程执行时,先把变量从内存读取到线程自己的本地内存空间,然后再对该变量进行操作。
  • 对该变量完成操作后,在某个时间再把变量刷新回主内存中,所以线程A释放锁后会同步到主内不超重,线程B获取锁后会同步主内存数据,即A线程释放锁,B线程获取锁,可以实现AB线程之间的通信。

1、volatile主要应用在多个线程对实例变量更改的场合,刷新主内存共享变量的值从而使得各个线程可以获得最新的值,(被volatile修饰的变量被修改后可以立即同步到主内存中,每次在用之前都是从主内存进行刷新);synchronized是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2、volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别的。
3、volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞
4、volatile仅能实现变量的修改可见性,不能保证原子性;synchronized可以保证变量的修改可见性和原子性,因为线程获得锁才能进入临界区,从而保证临界区内的所有语句全部执行。
5、volatile标记的变量不会被编译器优化,保证有序性;synchronized保证有序性的方法是保证同一时刻只允许一条线程操作。

lock和synchronized的区别?lock是哪个包下的方法?

答:可以说lock的出现是为了弥补synchronized的效率低的缺陷。(由于synchronized只允许一个线程进入)lock更诠释了锁的概念(加锁,解锁)

区别:
1、存在层面:synchronized是java中的一个关键字,存在于jvm层面;lock是java中的一个接口;
2、锁的释放条件:synchronized1、获取锁的线程执行完同步代码块后,自动释/2、线程发生异常时,jvm会自动释放锁;lock必须在finally关键字中释放锁,不然容易造成线程死锁;
3、锁的获取:synchronized中,假设线程A获得锁,B线程等待。如果A发生阻塞,那么B会一直等待。lock中会分情况而定,lock中有尝试获取锁的方法,如果尝试获取到锁,则不用一直等待。
4、锁的状态:synchronized取法判断锁的状态;lock可以判断;
5、锁的类型:synchronized是可重入、不可中断、非公平锁;lock是可重入、可判断、可公平锁;
6、锁的性能:synchronized使用于少量同步的情况下,性能开销比较大;lock锁使用于大量同步阶段。(在竞争不是很激烈的情况下,synchronized性能优于lock)

public interface Lock {
    void lock();//获取锁,会阻塞
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();//有返回值,不会阻塞
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//指定拿到锁的时间
    void unlock();//释放锁
    Condition newCondition();
}

java.util.concurrent.locks

说说内存模型,类加载器和GC垃圾回收机制

答:

  • 内存模型:
    为了保证共享内存的正确性(可见性原子性有序性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。解决了CPU多级缓存,处理器优化,指令重排序等导致的内存访问问题。
    java内存模型:符合内存模型规范的,屏蔽了各种硬件和操作系统差异的访问差异的,保证了java程序在各种平台下对内存的访问都能保证效果一致的机制和规范。
    java内存模型规定了所有变量都存储在主内从中,每条线程还有自己的工作内存,工作内存中保存了主内存共享变量的副本拷贝,线程对变量的操作都必须在工作内存中进行,而不能直接读写主内存。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递需要自己的工作内存与主内存之间进行数据同步。JMM就作用于同步过程,它规定了何时进行数据同步以及如何进行数据同步。
  • 类加载器:启动类加载器,扩展类加载器,应用类加载器。
  • GC垃圾回收机制: 1、将内存中不再被使用的对象进行回收; 2、按照新生代旧生代的方式来对对象进行回收; 3、主要回收区域:堆,方法区;
    4、对象被标记为垃圾的方法:可达性分析法;引用计数法
    5、垃圾回收算法:(1)标记清除算法(2)复制算法(3)标记整理算法(4)分代回收算法 6、Full
    GC的条件:jvm会首先检查老年代的连续空间是否大于新生代对象总大小或历次晋升的平均大小,如果条件成立首先进行minor gc,否则full GC;

可作为GCroot的对象都有什么?

答:1、java虚拟机栈中的引用的对象(每个方法执行的时候,jvm都会创建一个相应的栈帧,栈帧包括操作数栈、局部变量表、运行时常量池的引用;栈帧中包含这个方法内部使用的所有对象的引用;一旦该方法执行完后,该栈帧就会从虚拟机栈中弹出,这样一来局部对象的引用就不存在了,所以这些对象在下一次gc时就会被回收掉)
2、方法区中的类静态属性引用的对象(一般指被static修饰的对象,加载类的时候就加载到内存中)(static)
3、方法区中的常量引用的对象(final)
4、本地方法栈中JNI(native方法)引用的对象

方法区中的数据什么时候被回收?
方法区(永久代):
永久代的回收有两种:常量池中的常量、无用的类信息;
常量的回收很简单,没有引用了就可以被回收;
对于无用的类进行回收,必须保证3点:
1、类的所有实例都已经被回收
2、加载类的ClassLoader已经被回收
3、类对象的Class对象没有被回收(即没有通过反射引用该类的地方)
永久代的回收并不是必须的,可以通过参数来设置是否对类进行回收

内存泄漏已经发生怎么解决?

答:内存泄漏:一个不再被程序使用的对象或变量还在内存中占用存储空间
原因:1、静态集合类
2、当集合里面对象属性被修改后,再调用remove方法不起作用
3、监听器
4、各种连接
5、内部类和外部模块的引用
6、单例模式

解决方案:
1、修改JVM的启动参数,直接增加内存
2、检查错误日志,检查OOM异常之前是否有其他错误;
3、对代码进行分析,找出可能发生问题的部分;可能有以下几点
a:检查对数据库查询时看是否有一次查询,一次取出所有数据,所以数据库的查询尽量采用分页查询;
b:检查代码中是否有死循环或者递归调用
c:检查是否有大循环会重复产生新对象实体
d:检查集合对象是否有使用完会未清除
4、使用内存查看工具动态查询
5、使用参数命令查看

既然有GC机制,为什么还会发生内存泄漏

答:理论上java中有GC机制,不会存在内存泄漏问题;
无用但可达的对象;
一级缓存中的对象属于持久态,垃圾回收器不会回收,如果不及时关闭和清空,就可能导致垃圾泄漏

怎么能知道内存泄漏的时机?

答:1、首先top free df三连,查看状态
2、jstat -gc pid【interval】命令查看java进程的GC状态
3、jstack pid>jstack.log保存线程栈的现场,使用jmap -dump:format=b、file=heap.log pid保存现场
4、使用jstat [-options] pid interval 查看:
class查看类加载信息
compile编译统计信息
gc垃圾回收信息
gcXXX各区域GC的详细信息
jstat(强大的jvm监控工具)

分析栈
下载dump文件
使用MAT分析jvm heap
分析代码

内存分配策略?

答:1、静态分配:主要存放静态数据,全局static数据和常量。且该块内存在编译时就确定了。而且在程序运行期间都是存在的;
2、栈区:当方法执行时,会被放到栈的顶部,该方法的局部变量都会在栈内被创建,该方法执行完后回收;
3、堆区:又称动态内存分配,主要存储对象(即new出来的对象),如果存在不使用的对象,就可以GC掉

类加载机制说一下

答:

一、什么时候需要加载一个类呢?
1、new,getstatic,putstatic(设置类的静态字段)或invokestatic(调用一个类的静态方法时)这四个字节码需要先触发其初始化;
2、使用java.lang.reflect包的方法对类进行反射调用时,如果没有初始化先初始化其初始化;
3、当初始化一个类时,如果发现其父类还没有初始化,先触发其父类的初始化;
4、当虚拟机启动时,用户需要指定一个要执行的主类

二、类加载过程
java文件通过javac(java语言编程编译器)——class文件通过jvm(java解码器)编译
1、类装载阶段:磁盘–内存
类加载器主要分配三种:启动类加载器,扩展类加载器,应用类加载器
双亲委派模型:先父后子
优点:安全性;避免类的重复加载
2、链接阶段
(1)验证:验证魔数(标记类文本信息)
(2)准备:给static变量开辟内存,给定类型默认值
(3)解析:将间接引用改为直接引用的过程
3、初始化过程
静态变量的赋值操作

加载–验证–准备–解析–初始化–使用–卸载

谈谈你的项目

答:概括;
功能;
架构;
规模;
责任;

如何进行项目开发

谈谈你对Spring的理解

答:1、Spring是一个开源的业务层框架,分模块一站式框架,他能够整合各种其他主流框架;
2、Spring的实质就是一个实现了工厂模式的工厂类,在其配置文件中通过添加bean标签来创建实例对象。
3、Spring的核心,IOC AOP
IOC控制反转,将对象的创建全部交给Spring管理,Spring容易通过依赖注入的方式注入给调用者,这样做的好处是让bean与bean之间通过配置文件组织在一起,而不是通过硬编码耦合在一起;
依赖注入的方式有三种:接口注入,静态工厂注入,普通工厂注入
AOP面向切面编程,可以实现不该面源代码的前提下对功能进行扩展;实现原理是动态代理(JDK动态代理和CGlIb动态代理)

你还有什么问题吗?

贵公司的竟升机制是什么
对新人的培养机制是什么

Ajax

答:
概念:异步的JavaScript和xml,
作用:使用ajax可以不用刷新页面就能完成刷新
使用场景:
怎么实现:xml
异步的js和xml

ConcurrentHashMap

答:

1、产生背景:

Hashmap线程不安全;HashTable线程安全代价太大

2、源码分析

ConcurrentHashMap采用了非常精妙的分段锁策略,ConcurrentHashMap的主干是Segment数组
Segment继承了ReentrantLock,是一种可重入锁。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑竞争的。(比如默认ConcurrentLeve为16(2的幂),理论上就允许16个线程并发执行)
所以对于同一个Segment的操作才需要考虑线程同步,不同的Segment则无需考虑

Segment类似于HashMap,一个Segment维护一个HashEntry数组
hashEntry:(final)hash,(final)key,(volatile)value,(volatile)next
Segment:lf(负载因子),threshold(阈值),tab(主干数组即hashEntry数组)

put方法(加锁):
1、定位segment并确保定位的Segment已初始化;
2、调用Segment的put方法
如果需要扩容只会对Segment扩容

get方法:
由于涉及的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据

解决HTTP无状态

答:解决HTTP无状态其实就是进行会话跟踪
四种方法:URL 隐藏表单域 cookie session

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值