Java面经
-
java中有哪些集合?
安全:Vector,HashTable,ConcurrentHashMap
非安全:ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet
-
HashMap底层实现及扩容
底层:通过数组+链表+红黑树实现,初始容量是16,加载因子是0.75(超过16*0.75时扩容),加载因子过小会造成浪费,过大会使put,get碰撞几率增加,链表长度超过8时,链表就会转换为红黑树以提高检索速度,提高并发性能
-
ConcurrentHashMap的理解
采用分段锁机制,将map分成N个小的HashTable(线程安全,每个方法都加synchronized),根据key,hashcode决定放入哪个hashtable中
hash冲突(对某个元素进行hash运算后,得到地址但是发现改地址已经被使用)解决方法:开放定址法(重发上述操作,继续寻找下一个),链地址法
-
CAS的理解及ABA问题
CAS(compare and sort):是乐观锁的实现方式,如果多个线程CAS更新同一个变量,只有一个线程能执行,其他线程都会失败,失败的线程可以再次尝试、有内存地址V,预期原值A,新值B,V==A则V=B否则不会执行
ABA:线程E,F同时拿到了内存地址内的变量A,因为时间差问题F将变量A改成B又改回A,E线程最后也对A成功进行了修改。
解决:在jdk1.5后提供AtomicStampedReference解决ABA
CAS缺点:ABA问题,循环时间长,开销比较大,只能保证一个共享变量的原子操作(锁去解决),浪费cpu资源
-
AQS的理解
抽象队列同步器,其是并发包中的基础组件,用来实现各种锁,ReentrantLock底层由其实现包括 state变量,等待队列,加锁线程三个核心内容
实现原理:初始时state=0,加锁线程为null,线程1加锁后利用CAS判断state递增是否成功若成功加锁队列由null变为线程1,如果此时线程2也想加锁,但是state已经被使用则其进入等待队列之中,若线程1释放锁(state递减,若state=0,则彻底释放锁加锁线程为null)则从等待队列的表头唤醒线程2,重复线程1操作,如果成功则从等待队列中出列加锁线程变为线程2
-
事务的隔离级别
脏读:一个事务读到另外一个事务尚未提交的数据
幻读:以相同的条件,检索以前检索过的数据,发现其他事务插入了新的数据
不可重复读:一个事务在读取后的某个时间再次去读数据发现数据改变
Read uncommit 都没解决
Read commit 只解决了脏读
repeat read 没有解决幻读
serializable 都解决,最高事务级别
-
sleep和wait的区别
sleep:让线程睡眠,不会释放锁,可以在任何地方使用,自动唤醒
wait:让线程等待,会释放锁,只能与synchronized配合使用,需要手动唤醒
-
java内存管理
java的内存管理交给jvm去分配和回收,有虚拟机栈(存放一些运行时的数据),本地方法区(专门为c/c++使用),程序计数器(记录程序中线程执行的字节码地址),方法区 (存放静态资源,常量池等),堆(存放实例对象等)
-
redis的使用场景及举例
排行榜,订单自动回收(配置redisMessageListenerContainer这个Bean,创建事件过期的监听类,重写onmessage方法),最近访问记录,计数
-
springboot和springmvc区别
springboot基于spring开发,以约定大于配置为思想,帮助我们设置,管理参数,内嵌服务器没有冗余代码和繁琐的xml配置
-
慢sql及解决
查询时没有走索引,查询时间过长导致慢sql
解决:对查询性能进行优化
-
快排思路
package sort; public class Quick { /* 比较v元素是否小于w元素 */ private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; } /* 数组元素i和j交换位置 */ private static void exch(Comparable[] a, int i, int j) { Comparable t = a[i]; a[i] = a[j]; a[j] = t; } //对数组内的元素进行排序 public static void sort(Comparable[] a) { int lo = 0; int hi = a.length - 1; sort(a, lo, hi); } //对数组a中从索引lo到索引hi之间的元素进行排序 private static void sort(Comparable[] a, int lo, int hi) { //安全性校验 while (hi < lo) { return; } //需要对数组中lo索引到hi索引处的元素进行分组(左子组和右子组); int index = partition(a, lo, hi);//返回的是分组的分界值所在的索引,分界值位置变换后的索引 //让左子组有序 sort(a, lo, index - 1); //让右子组有序 sort(a, index + 1, hi); } //对数组a中,从索引 lo到索引 hi之间的元素进行分组,并返回分组界限对应的索引 public static int partition(Comparable[] a, int lo, int hi) { //确定分界值 Comparable key = a[lo]; //定义两个指针,分别指向待切分元素的最小索引处和最大索引处的下一个位置 int left = lo; int right = hi + 1; //切分 while (true) { //先从右往左扫描,移动right指针,找到一个比分界值小的元素,停止 while (less(a[--right], key)) { if (right == lo) { break; } } //再从左往右扫描,移动left指针,找到一个比分界值大的元素,停止 while (less(key, a[++left])) { if (left == hi) { break; } } //判断 left>=right,如果是,则证明元素扫描完毕,结束循环,如果不是,则交换元素即可 //交换分界值 if (left >= right) { break; } else { exch(a, left, right); } } exch(a, lo, right); return right; } }
-
synchronized+静态方法和成员方法有什么区别?
synchronized+静态是类锁,让静态方法之间实现同步
+成员方法是对象锁,让实例对象之间同步
方法锁,类成员变量的作用
-
线程池有哪些类型?如果线程池满了再往里面塞任务会向哪个位置存放?有哪些策略?
-
线程池原理:任务到来时,先判断核心线程数是否已满,如果没满则执行,如果已满则判断任务队列是否已满,若没满则存放在任务队列,若已满则比较线程池最大容量,如果没满则创建线程继续执行,如果已满就判断
-
newCachedThreadPool适合执行大量的耗时短的任务,newFixedThreadPOOL定长线程池只有核心线程并且不会被回收,newSchedulexdThreadPool定长线程池,核心线程数固定,非核心线程在空闲时会回收,newSIngleThreadExecutor单线程化的线程池使用无界队列让多个任务在同一个线程中有序执行
-
如果满了的话会将池队列(阻塞队列),有四种策略
AbortPolicy丢弃任务,直接抛出异常
CallerRunsPolicy直接执行
DiscardPolicy忽视任务
DiscardOldestPolicy丢弃队列里面最近的任务然后执行这个任务
-
-
堆和栈的区别
堆:存放对象,由开发人员释放,内存地址由低到高,堆动态分配
栈:存放静态资源,局部变量,函数返回地址,参数等,自动释放,内存地址由高到底,栈静态分配和动态分配
-
方法加锁和对象加锁区别
方法锁:让成员变量的访问同步
对象锁:让实例对象之间同步
-
多用户并发怎么解决?
利用悲观锁(适合写多读少,每次访问数据时,该数据都可能被其他线程访问)或者乐观锁(无锁,提交时判断是否冲突)
-
分段锁的理解
优点:不同段的map都能并发执行
缺点:分成很多段时,容易造成内存空间不连续或者碎片化,操作map时竞争同一个锁的概率很小,容易造成更新等操作时间过长,分段锁性能降低
-
垃圾回收算法有哪些?垃圾回收器(G1,CMS)
有四种种,常用的是3中(引用计数不使用)都是基于可达性分析算法,有复制算法(把存货对象复制到另外一块儿区域空间利用率只有一半适合适合新生代对象无法创建时触发),标记整理算法(适合老年代,对存活的对象进行移动,从做后一个对象的地址开始删除后续地址,可以再次申请,缺点是要移动大量的对象,优点是没有内存碎片),清除算法(只清除死亡的对象)
复制算法(第一次触发GC把存活的对象拷贝到from,第二次触发会对eden和from区进行垃圾回收,将存活的对象复制到to区域(如果有 对象的年龄已经达到了老年的标准则复制到老年代),对象的年龄加一,然后清空eden和fron区域,复制后有交换谁空谁是to,最后交换from和to区) 没有产生内存碎片,会造成空间的浪费
标记清理(先标记后清除,会造成内存碎片)
标记整理(先标记后整理,再清除空闲的内存空间)GC有七个垃圾回收器
Serial,ParNew,Parallel,parallelOld,CMS,SerialOld,G1
-
泛型需要注意什么?
引用传递问题,不能使用基本数据类型,类型擦除问题,静态方法类中使用问题
-
springboot核心组件及自动装配用到的注解
启动器,自动装配和监测器
@Configuration@componentScan@EnableAutoConfiguration
-
索引的底层数据结构?
底层数据结构可以是线性表,红黑树,B+树等但选用B+树是因为其降低节点度的同时,树的深度很低提高了检索速度,提高了并发性能。
-
微服务
Eureka,负载均衡(ribbon 使用http的rest方式,feign接口的方式),zuul,hystrix,spring cloud config
springcoloud与dubbon的区别
注册中心:Eureka,zookeeper
服务调用方式:rest,rpc
服务监控:spirngboot admin,Dubbon minintor
断路器:hystrix,无
分布式配置:springcloud config,无
服务网关:zuul,无
-
乐观锁和悲观锁
乐观锁:无锁,在数据提交时才去判断是否发生冲突,version
悲观锁:每次访问数据时,都需要加锁
-
springboot设计模式
设计模式:单例模式,模板模式,代理模式,适配器模式,观察者模式,工厂模式
-
springmvc工作流程
DispatcherServlet接收到请求后交给处理器映射器,处理器映射器找到具体的适配器生成处理器对象交给DispatcherServlet,其再调用处理器适配器去调用具体的controller,执行完成后返回给ModelAndView,处理器适配器将结果返回给DispatcherServlet,其将ModelAndView传到视图解析器,视图解析器解析后返回给view,DispatcherServlet通过view进行渲染视图,DispatcherServlet响应用户
-
ioc和aop理解
ioc:控制反转,使用map容器管理Bean的整个生命周期,依赖注入,有三种注入方式(构造器,setter,属性注入)
aop:面向切面编程。再不改变原有代码的情况下对程序的功能进行增强,常见应用场景:日志,事务,监控等
Bean生命周期:
ioc实现原理:
aop实现原理:
-
为什么使用synchronized而不使用ReentrantLock?
可重入锁:判断所有没有被锁上,再判断是谁锁上的,若是自己锁上的可以再次访问临界资源并继续加锁,加锁和解锁次数一样
不可重入锁:判断锁有没有被锁上
synchronized:关键字,由jvm支持,A获取锁后B会一直等待直到释放锁,竞争不激烈其,释放锁有两种途径:获取锁的线程执行完毕或执行代码出现异常
reentantLock:可重入锁,其是类,竞争激烈,有三种获取锁的方式:lock(),lockinterruptibly()可中断,tryLock()尝试获取锁,在一定等待时间内如果没有获取到则中断 ,通过unlock()释放锁
使用synchronized而不使用ReentrantLock原因:1.减少内存开销(后者节点通过继承AQS来获取同步支持)2.前者内部优化,能在运行时做出优化措施(锁粗化,锁自旋,锁消除)
-
mysql中金额等精密的数据类型用哪种类型存储?
bigdecimal,decimal
-
sql语句使用where时尽量不要使用哪些语法?
不要使用where1=1,其会对表中的每行数据进行索引,造成很大的性能损失,使用后无法使用索引等优化策略,当数据量很大时,查询速度很慢
-
MVCC理解及实现原理
mvcc:多版本并发控制,是一种解决读写冲突的无所并发机制,提高并发性能,让其并发操作数据库时,读操作不阻塞写操作,写操作不阻塞读操作,解决了脏读幻读不可重复的等隔离问题
实现原理:在表中默认创建了三个字段分别代表事务id,索引,和指向日志的指针,用排它锁锁定该行记录,将记录复制到undolog日志里面然后进行事务,若成功则提交,不成功则根据指针找到日志中存储的信息
-
spring是如何管理实务
通过aop管理实务,根据aop通过实现TransactionInterceptor调用invoke实现具体逻辑
1.先做准备工作,根据注解判断哪些事物需要执行
2.关闭自动提交,获取连接,开启事务
3.执行sql逻辑
4.若执行失败,获取连接对象,调用rollback方法回滚成功则commit提交
5.事务执行完成后清除相关事务信息
-
object中常用方法
toString,equals,clone,hashcode,wait,notify,finalized
-
深浅拷贝
浅拷贝新生成的对象和就对象都指向同一个地址而深拷贝各自指向各自的地址
-
jdk新特性
lambda,匿名内部类,stream,时间日期API,函数式编程
-
代理实现方式
静态代理
动态代理:运行时对一些方法进行代理,面向接口的动态代理没有接口则不可用。MyBATIS
第三方CGILB代理:面向父类的动态代理,在不修改原有代码的基础上利用继承对方法进行代理,有无接口都可以使用可以读取类上注解 aop
-
Spring是如何解决循环依赖的?
让对象的实例化和初始化分开,先完成对象的实例化,提前暴露对象的引用在程序调用过程中如果碰到某个对象的引用则对该对象优先进行赋值
三级缓存:一级:存放完整的对象,二级:存放实例化但未初始化的对象,三级:三级缓存,存放的是 Bean 工厂,主要是生产 Bean,存放到二级缓存中。而三级缓存结构就是在第三级缓存完成bean的扩展,生成代理对象,放入二级缓存之中,供其他bean获取。当不需要实现AOP的时候,解决循环依赖不用三级缓存机制,也不用单例工厂,二级缓存就足以实现。
不需要实现AOP时Spring解决循环依赖基本流程:
假设单例一与单例二相互依赖对方并且此时都没有加入到单例池
1 创建单例一
2 将单例一加入earlySingletonObjects缓存
3 自动装配单例二
4 判断单例二在earlySingletonObjects缓存是否存在
5 不存在则创建单例二
6 将单例二并且加入earlySingletonObjects缓存
7 自动装配单例一
8 判断单例一在earlySingletonObjects缓存是否存在
9 明显第2步已经加入earlySingletonObjects缓存
10 注入成功,单例2创建完成并加入singletonObjects单例池
11 注入成功,单例1创建完成并加入singletonObjects单例池假设单例1与单例2相互依赖对方并且此时都没有加入到单例池
1 创建单例一
2 创建单例一的工厂对象并且加入singletonFactories缓存
3 自动装配单例二
4 通过 getSingleton方法 判断单例二在三级缓存中是否存在
5 明显此时单例一不存在,所以创建单例二
6 创建单例二的工厂并且加入singletonFactories缓存
7 自动装配单例一
8 通过 getSingleton方法 判断单例一在三级缓存中是否存在
9 明显第2步已经加入singletonFactories缓存
10 单例二进行AOP处理
11 注入单例一成功,单例二创建完成并加入singletonObjects单例池
12 注入单例二成功,单例一创建完成并加入singletonObjects单例池 -
BeanFactory和FactoryBean区别
都可以用来创建Bean但是前者严格按照Bean的生命周期进行创建
后者可以实现一些简单的自定义对象的创建
-
内存溢出
堆溢出(分配的对象超过堆的大小),栈溢出(方法套娃,不断调用栈帧过多),方法区溢出(方法区内的数据过大),堆外溢出(需要手动申请和释放)
-
为什么不使用finalize方法?
1.其执行线程优先级很低
2.该方法只能使用一次
-
如何判断对象是否存活?
通过可达性分析算法,定义GC Roots集合用链路追踪的方式查找,或者根搜索路径法
-
扩容新生代为什么能提高GC检索效率?
因为其可以可以拉长GC间隔
-
自增是线程安全吗?如何实现自增?
不安全,通过加锁来实现
-
hashset没什么可以去重?
默认引用类型比较的是地址值,如果地址值相同,那么这是一模一样的对象,那么判断肯定是同一对象,就不再重复的往里放。
如果对象重写了hashcode和equals方法,就比较这二者是否相等。
如果hashcode相等,那么比较equals方法,如果equals再相等,那么证明这是同一个对象;如果equals不相等,那么及时hashcode相等,也是两个对象。
HashSet确定相同的方式其实就是HashCode相同(才能找到同一链表),然后equals的返回值(才能比较具体节点进行覆盖)。 -
为什么atomicIneger底层使用CAS不用synchronized?
A.synchronized采用的是悲观锁,是一种独占锁,独占锁就意味着 其他线程只能依靠阻塞[就是其他线程不停的询问]来等待线程释放锁。而在 CPU 转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起 CPU 频繁的上下文切换导致效率很低
B.CAS采用的是一种乐观锁的机制,它不会阻塞任何线程,所以在效率上,它会比 synchronized 要高。所谓乐观锁就是:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。 -
使用无界阻塞队列会出现什么情况?
无界阻塞队列:LinkedBlockingQueue默认的最大任务数量是Integer.MAX_VALUE,如果线程池内的线程在获取到一个任务后,需要执行时间较长,会导致workQueue里积压的任务越来越多
远程服务调用,使用无界阻塞队列,如果线程池内的线程接到一个任务后,需要执行时间较长,大量任务的积压导致机器的内存使用不停的飙升,最后会导致OOM
案例:无界阻塞队列在Java中一个具体的使用场景
public static ExecutorService newSingleThreadExecutor(){ return new FinalizableDelegatedExecutorService( new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>())); ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
参数:corePoolSize和maxPoolSize设置为1,线程池内只有一个线程
参数:通过LinkedBlockingQueue无界队列将任务进行排队,保证了串行执行所有任务,所有任务肯定是绝对顺序执行使用线程池时慎用无界队列,一般情况下针对特定场景才会进行使用
-
java反射原理,注解原理
反射机制:在运行时(Run-Time),对于任何一个对象都知道其所有属性及方法,并能够调用。
反射原理:每一个类都有一个Class对象。所有类都是在第一次使用时,动态加载到JVM中,当程序创建第一个对类的静态成员引用时,会加载这个类。一旦你一个类的Class对象被载入到内存,它便被用来创建这个类的所有对象。Java使用Class对象来执行其RTTI(Run-Time Type Identification),这也是反射的基础。
注解:为某一固定的处理方法提供坐标,使得处理方法可以通过反射机制获取到注解的方法或其他属性。
注解原理:获取到某个类的Class对象后,使用反射机制获取到该类的属性、方法等,判断对应的属性、方法是否使用了特定的注解,如果有则调用该类注解的处理器来处理。