阿里一面

说一下ArrayList和LinkedList的区别

  1. 首先,他们的底层数据机构不同,ArrayList底层是数组实现的,LinkedList底层是基于链表实现的
  2. 由于底层数据结构不同,他们使用的场景也不同,ArrayList更适合随机查找,ListedList更适合删除和添加,查询,添加,删除的时间复杂度不同
  3. 另外ArrayList和LinkedList都实现了list接口,但是LinkedList还实现了Deque(迪克)接口,所以,LinkedList还可以当作队列来使用

说一下ConcurrentHashMap的扩容机制

1.7版本

  1. 1.7版本的ConcurrentHashMap是基于Segment(洗个门t)分段实现的
  2. 每个Segment相当于一个小型的HashMap
  3. 每个Segment内部进行扩容,和HashMap的扩容逻辑类似
  4. 先生成新的数组,然后转移元素到新的数组中
  5. 扩容的判断也是在每个Segment内部单独判断的,判断是否超过阈值

1.8版本

  1. 1.8版本的ConcurrentHashMap不在基于Segment实现
  2. 当某个线程进行put时,如果发现ConcurrnetHashMap进行扩容那么该线程一个经行扩容
  3. 如果某个线程put时,发现没有经行扩容,则将Key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过则进行扩容
  4. ConurrentHashMap是支持多个线程同时扩容的
  5. 扩容之前先生成一个新的数组
  6. 在转移元素时,先将原数组分组,将每组分给不同的线程来经行元素的转移,每个线程负责一组或多组的元素转移工作

    说一下HashMap的put方法

    先说HashMap的Put方法的大致流程:

  7. 根据Key通过哈希算法与运算得出数组的下标

  8. 如果数组下标的元素时空,则将key和value封装为Entry对象(1.7时entry对象,1.8是Node对象并放入该位置
  9. 如果数组下标不为空,则要分情况讨论
  • 如果是JDK1.7,则先判断是否需要扩容,如果需要扩容就进行扩容,如果不进行扩容就生成Entry对象,并使用头插法添加到当前位置的链表中
  • 如果是JDK1.8,则先判断当前位置上的Node的类型,看是红黑树还是链表Node

    1. 1. 如果是红黑树Node,则将keyvalue封装为一个红黑树节点并添加到红黑树 中,如果当前过程中会判断红黑树中是否存在当前的Key,如果存在,则更新 value。<br /> 2. 如果此位置上的Node对象是链表节点,则将keyvalue封装成一个Node并通 过尾插法插入到链表的最后位置,因为是尾插法,所以需要遍历链表,在遍历链 表的过程中会判断是否存在当前的key,如果存在则更新当前的Value,当遍历完 链表后,将新的链表节点插入链表中,插入链表后,会看当前链表的节点数,如 果大于8,那么就会将该链表转成红黑树<br /> 3. keyvalue封装为Node插入后链表或者红黑树中后,在判断是否需要经行 扩容,如果需要就扩容,如果不需要就结束put方法

说一下ThreadLocal

  1. ThraedLocal是java中所提供线程本地储存机制,可以利用该机制将数据缓存到线程内部,该线程可以在任意时刻,任意方法中获取缓存的数据
  2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)都存在一个ThreadlocalMap,Map的key为ThreadLocal对象,Map的Value是需要缓存的值
  3. 如果线程中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该把设置的key,跟value,也就是Entry对象进行回收,单线池中的线程是不会回收的,而出现内存泄漏,解决办法是,使用ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象

    4.ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享一个连接)
    image.png

    说一下JVM中,那些是共享区,那些可以作为gc root

    1.堆区跟方法区是所有线程共享的,栈,本地方法栈,程序计数器是每个线程独有的
    src=http%3A%2F%2Fimg2018.cnblogs.com%2Fblog%2F832130%2F202001%2F832130-20200123151811911-5421006.jpg&refer=http%3A%2F%2Fimg2018.png
    2.什么是gc root,jVM在垃圾回收时,需要找到垃圾的对象,也就是没有被引用的对象,但是直接找垃圾对象比较耗时,所以反过来,先找非垃圾对象,也就是正常对象,那么就要从某些根开始查找,根据这些根的引用路径找到正常对象,而这些跟有一个特征就是他会引用其他对象,而不会被其他对象引用,例如:栈中的本地变量,方法区中的静态变量,本地方法中的变量,正在运行的线程等可以作为gc root;

项目中如何排查JVM问题

对于还在运行的系统

  1. 可以使用jmap来查看JVM中各个区域的使用情况
  2. 可以通过jstack来查看线程的运行情况,比如那些线程阻塞,是否出现死锁
  3. 可以通过jstat命令来查看垃圾回收的情况,特别时fullgc。如果发现fullgc比较平凡就要调优了
  4. 通过各个指令的结果,或者用jvisualvm等工具进行分析
  5. 首先猜测fullgc的频繁的原因,如果频繁发生fullgc一直没有内存溢出,那么就表示fulklgc回收了很多对象,所以这些对象最好在yonggc中回收掉,避免进入老年代,对于这种情况,就要kaolv这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少了,就证明修改有效
  6. 同时,还要找到占用cpu最多的线程,定位到具体方法,优化方法的执行,看能否避免某些对象的创建,从而节省内存

对于放生OOM的系统

  1. 一般生产系统中都会设置当系统发生OOM时,生成当前的dump文件(-XX:+HeapDumpOnOutOfMemoryError-XX:HeapDumpPath=/usr/local/base)
  2. 我们利用利用jsisualvm等工具分析dump文件
  3. 根据dump文件找到异常的实例对象,和异常的线程(占用cpu高),定位到具体的代码
  4. 然后在进行详细的分析和调试
  5. jvisualvm安装Visual GC插件 jvisualvm安装Visual GC插件.pdf

    如何查看线程死锁

  6. 可以通过jstack命令来进行查看,jstack命令中就会显示死锁的线程

  7. 或者两个线程去操作数据库时,数据库发生死锁,这时可以查看数据库里面的死锁情况
    B1、查询是否锁表
    show OPEN TABLES where In_use > 0;
    2、查询进程
    show processlist;
    3、查看正在锁的事务
    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; 
    4、查看等待锁的事务
    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
    

线程之间时如何通讯的

  1. 线程之间时可以通过共线内存或者基于网路来进行通讯
  2. 如果通过共享内存来进行通讯,则需要考虑并发问题,什么时候阻塞,什么时候唤醒
  3. 想java中的wait().notify()就是阻塞和唤醒
  4. 通过网路就比较简单,通过网路连接将数据发送给对方,然后也要考虑并发问题,处理方式就是加锁等方式

介绍一下spring,读过源码介绍一下大致流程

  1. spring是一个快速开发框架,spring帮助程序员来管理对象
  2. spring的源码实现是非常优秀的,设计模式的应用,并发安全的实现,面向接口设计等等
  3. 在创建spring容器,也就是spring启动时:
  • 首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中
  • 然后筛选出非懒加载的单例BeanDefinition进行创建bean,对于多例Bean不需要在启动过程中创建,对于多例bean会在每次获取Bean时利用BeanDefinition去创建
  • 利用BeanDefinition创建Bean就是创建Bean的生命周期,这期间包括了合并BeanDefinition、推断构造方法,实例化。属性填充,初始化前,初始化,初始化后等步骤,其中AOP就是发生在初始化后的这一步骤

    4.单例bean创建完了以后,spring会发布一个容器启动事件
    5.审判日那个启动结束
    6.在spring源码中,很负责。比如源码中会提供一些模板方法,让子类实现,比如源码中还涉及到一些BeanFactoryPostProcessor和BeanPostProcesser的注册,spring扫描就是通过BeanFactoryPostProcesso来实现的,依赖注入就是通过BeanPostProcesser来实现的
    7.spring启动过程中还会区处理@Import等注解
    Spring启动流程详解.png

说一下spring的事务机制

  1. Spring事务底层是基于数据库事务跟AOP机制的
  2. 首先对于使用@Transactional注解的bean,spring会创建一个代理对象作为Bean
  3. 当调用代理对象的方法时,会先判断方法上是否加了@Transactional注解
  4. 如果加了,利用事务管理器建立一个数据库连接
  5. 并且修改数据库连接的autocommit属性为false,禁止此连接自动提交,这就是实现spring事务非常重要的一步
  6. 然后执行当前方法,方法中执行sql
  7. 执行完当前方法,如果 没有异常就直接提交事务
  8. 如果有异常,并且这个异常需要回滚的就会回滚事务,否则仍然提交事务
  9. spring事务隔离级别对应的就是数据库的隔离级别
  10. spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为一个新开的事务,那么就是建立一个数据库连接,在此新数据库连接上执行sql


什么时候@Transactional失效

因为spring事务是基于代理来实现的,所以某个加了@Transactional的方法只有被代理对象调用时,注解才会生效,所以如果时被代理对象来调用这个方法,那么@Transactional是不会生效的
同时如果某个方法是private的,那么@Transactional也会失效,因为底层cglib是基于父子类实现的,子类是不能被父类的private的方法的,所以无法很好的利用代理,也会导致@Transactional失效

Dubbo是如何做系统交互的

Dubbo底层是通过RPC来完成服务和服务之间的调用的,Dubbo支持很多协议,比如默认的dubbo协议,比如http协议,比如rest等都是支持的,他们的底层使用的技术是不太一样的,比如dubbo协议使用的是netty,也使用mina,http协议底层是通过tomcat或者jetty
服务消费者调用某个服务时,会将当前调用的服务接口的信息,当前方法信息,执行方法所传入的入参信息等组装为一个Invocation对象,不同的协议通过不同的数据组织方法和传输方式将这个对象传给服务提供者提供者接受到对象后,找到对应的服务实现,利用反射执行对应的方法,得到方法结果后通过网络相应给服务消费者。

当然,Dubbo在这个调用过程中还做很多其他设计,比如服务容错,负责均衡,Filter机制,动态路由机制等等,让Dubbo能够处理更多企业中的需求
Dubbo底层原理 (1).png

Dubbo的负载均衡策略

dubbo目前支持的:

  1. 平衡加权轮询算法
  2. 加权随机算法
  3. 一致性哈希
  4. 最小活跃数算法

阿里二面


如何实现Aop,项目中那些地方用到Aop

利用动态代理技术实现Aop,比如JDK动态代理或者Cglib动态代理,利用动态代理技术,可以针对某个类生成代理对象,当调用对象的,某个方法时,可以在任意控制该方法的执行,比如可以打印执行时间,在执行该方法,并且该方法执行完成后,在此打印执行时间。
项目中,比如事务、权限控制、方法执行时长日志都是通过Aop技术来实现的,凡是需要对某些方法做统一处理,都可以用AOP来实现,利用AOP可以做到业务无入侵
AOP的工作流程.png

spring后置处理器的作用

Spring的后置处理器分为BeanFactory后置处理器和Bean后置处理器,他们是Spring底层源码框架结构设计中非常重要的一种机制,通知开发者也可以利用这两种后置处理器来进行扩展。BeanFactory后置处理器表示针对的beanFactory的处理器,spring启动过程中,会先创建出Beanfactory实例,然后利用Beanfactory处理器加工BeanFactory。比如spring的扫描就是基于Beanfactory后置处理器来实现的,而bean后置处理器也类似,spring初见bean的过程中,首先会实例化一个对象,然后利用bean的后置处理器对bean进行加工,比如我们常说的依赖注入就是利用bean的后置处理器来实现的,通过该bean后置处理器来给实例化对象中加了@autowired注解的属性赋值,比如我们常常说的AOP,也是利用一个bean的后置处理器来实现的,基于原实例对象,判断是否进行AOP,如果需要,那么就基于原实例对象进行动态代理,生成一个代理对象
AOP的工作流程.png

说说你理解的分布式锁的实现

分布式锁所要解决的问题本质是:能够对分布式在多台机器中线程对共享资源的互斥互问,在这个原理上有问对实现的方式:

  1. 基于mysql。分布式环境中的线程连接同一个数据库,利用数据库中的行锁来达到互斥访问,但是msql加锁跟释放锁的性能较低,不适合在真正的开发中使用
  2. 基于zookeeper,zookeeper中的数据是存在内存中的,所以相对于mysq性能上是合适与实际环境的,并且基于zookeeper的顺序节点,临时节点,Watch机制能非常好的适用于生产环境
  3. 基于redis,redis中的数据也是存在于内存的,基于redis的消费订阅功能,数据超时时间,lua脚本等功能,也能很好的实现分布式锁

    Redis的数据结构以及使用场景

    redis的数据结构有:

  4. 字符串:可以用来做简单的数据缓存,可以做简单的字符串,也可以缓存某个json格式的字符串,Redis分布式锁的实现就是利用这种数据结构,还包括可以实现计数器,Session共享,分布式ID

  5. 哈希表:可以用来储存一些key-value对,更适合用来存储对象
  6. 列表:redis的列表通过命令组合,既可以当作栈,也可以当作队列来使用,可以用来缓存类似与微信公众号,微博消息流数据
  7. 集合:和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交际、并集、差集操作,从而可以实现类似,我和某人共同关注的人,朋友圈点赞功能
  8. 有序集合:集合是无序的,有序集合可以设置顺序,可以实现排行榜的功能

Redis集群策略

  1. 主从模式:这个模式比较简单,主库可以读写,并且会和从库数据同步,这种模式,客户端可以直接连接主库或者从库,但是主库或者从库宕机后,客户但要从新手动修改IP,另外,这种方式比较难以扩容,这个集群所能储存的数据受限某台机器的内存容量,所以不支持特大数据量
  2. 哨兵模式:这种模式在中从模式上新增了一个哨兵节点,但主库节点宕机后,哨兵会发现主库节点宕机,然后在从库选择一个库作为新的主库,另外哨兵也可以做集群,从而保证某一个哨兵节点宕机后,还有其他哨兵可以工作,这种模式可以较好的保证redis集群的高可用,但仍然不能解决redis容量的上线问题。
  3. Cluster模式:cluster模式是用的比较多的模式,它支持多主多从,从模式上按照key进行槽位的分配,可以使得不同的key分散到不同的主节点上,利用这个模式可以整个集群支持更大的数据容量,同时每个主节点可以拥有自己多个从节点,如果该主节点宕机后,他会从从节点选择一个新的主节点

    Mysql数据库中,什么情况下设置了索引但是无法使用

  4. 没有符合左前缀原则

  5. 字段进行的隐士数据类型转换
  6. 走索引没有全表扫描高

https://www.bilibili.com/video/BV1W64y1u761?from=search&seid=6062298215110905390

京东一面


你遇到过那些设计模式

  1. 代理模式:mybatis中用到的JDK动态代理来生成Mapper的代理对象,在执行代理对象的方法是会执行SQL,Srping中AOP、包括@Configuration注解的底层实现都是用到了代理模式
  2. 工程模式:Spring中的beanFactory就是一种工程模式
  3. 适配器模式:Spring中bean的销毁的生命周期就用到了适配器模式,用来适配各种Bean销毁逻辑的执行方式
  4. 模板方法模式:Spring中的refresh方法中就提供给子类继承重新的方法,就是用了模板方式

    Java怎样避免死锁

    造成死锁的原因:

  5. 一个资源每次只能被一个线程使用

  6. 一个线程在阻塞等待某个资源时,不释放已占有的资源
  7. 一个线程已经获得资源,在未使用完之前,不能强行剥夺
  8. 如果线程形成头尾相接的循环等待资源关系

这是造成死锁的必须要达到的四个条件,如何避开死锁,只要不满足其中一个就行

开发过程中:

  1. 要注意加锁顺序,要保证每个线程按同样的顺序进行加锁
  2. 要注意加锁的时限,可以针对所设置的一个超时时间
  3. 要注意死锁检查,这一种预防机制,确保第一时间发现死锁,并进行解决

深拷贝和浅拷贝

深拷贝和浅拷贝是指对象的拷贝,一个对象中存在两种类型的属性,一种时基本数据类型,一种时实例对象的引用

  1. 浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向同一个对象
  2. 深拷贝是指,即会拷贝基本数据类型的值,也会针对实例对象的引用地址指向的对象进行复制,深拷贝出来的对象,内部的属性指向不是同一个对象

    简述线程池原理,FixedThreadPool用的阻塞队列时什么

    线程池内部时通过队列+线程实现的,当我们利用线程池执行任务时:

  3. 如果此时线程池中的数量小于corePoolSize,即使线程池中iing的线程都处于空闲状态,也要创建新的线程来处理被添加的任务

  4. 如果此时线程池中的数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓存队列
  5. 如果此时线程池中的数量大于corePoolSize,但是缓冲队列workQueue满,并且线程池中的数量小于maximunPoolSize,创建新的线程来处理被添加的任务
  6. 如果此时线程池中的数量大于corePoolSize,但是缓冲队列workQueue满,并且线程池中的数量小于maximunPoolSize,那么通过handler所指定的策略来处理任务
  7. 如果此时线程池中的数量大于corePoolSize,如果某些线程空闲时间超出keepAliveTime,线程将被终止,这样,线程池可以动态的调整池中的线程数

FixedThreadPool代表定长线程池,底层用的LinkedBlockingQueue,表示无界的阻塞队列

sychronized和ReentrantLock的区别

  1. sychronized是一个关键字,ReentrantLock是一个类
  2. syncronized会自动加锁与释放锁,ReentrantLock需要程序原手动释放锁
  3. syncronized是JVM曾面的锁,RenntrantLock是API层面的锁
  4. syncronized是非公平锁,RenntrantLock可以选择公平锁和非公平锁
  5. syncronized锁的是对象,所信息保存在对象头中,ReentrantLock通过代码中的int类型的state标识来识别锁的状态
  6. syncronized底层是一个锁升级的过程

    sychronized的自旋锁,偏向锁,轻量级锁,重量级锁,分别介绍跟联系

  7. 偏向锁:在锁对象的对象头中记录一下当前获取该锁的线程ID,该该线程下次如果来获取锁的时候可以直接获取到

  8. 轻量级锁:由偏向锁升级来的,当一个线程获取锁后,这把锁是偏向锁,此时如果由第二个线程来竞争锁,偏向锁会升级到轻量级锁,之所以是轻量级锁,是因为与与重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
  9. 如果自选多次仍然没有获取锁,则会升级到重量锁,重量级锁会导致线程阻塞
  10. 自旋锁:自选锁就是线程在获取锁的过程中,不去去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统进行的,比较消耗时间,自旋是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到则表示获取到锁了,这个过程线程一直运行,相对而言,没有使用太多的操作系统资源,比较轻量

    volatile关键字,他是如何保证可见性,有序性

  11. 对于加了volatile关键字的成员变量来说。对于变量进行修改时,会直接将cpu高级缓存中的数据写到主内存,对于变量的读取也会直接从主内存读取,从而保证可见性

  12. 在对volatile修饰的成员变量就行读写时,会插入内存屏障,而内存屏障可以达到禁止指令重排的效果,从而保证有序性

    mysql的锁你了解那些

    按照锁的颗粒度分类:

  13. 行锁:锁住某行数据,锁的颗粒度小,并发度高

  14. 表锁:锁整张表,锁的颗粒度大,并发低
  15. 间隙锁:锁的一个区间

可以分为:

  1. 共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务可以读,但是不能写
  2. 排他锁:也就是写锁,一个事务给某行数据加了写锁,其他数据不能读,也不能写

还可以分:

  1. 乐观锁:并不会正真锁某个记录,而是通过一个版本号实现
  2. 悲观锁:上面的行锁、表锁、等都是悲观锁

    事务的基本特征跟隔离级别

    事务的基本特征是ACID分别是:
    原子性指一个事务中要操作要么全部成功,要么全部失败。
    一致性是指一个数据库总是一个一致性状态转换都另一个一致性状态。比如A转账给B 100 元,加入A只有90元,支付之前我们的数据库的数据是否符合约束的,但是如果事务执行成功了,我们的数据库约束就破坏了,因此是不能成功,这也说明了事务提供一致性的保证。
    隔离性一个事务修改在最终提交前,对其他事务是不可见得。
    持久性指一旦事务提交,所有修改后的数据都会永久的保存在数据里。
    隔离性的4个特征,分别是
  • read uncommit 读未提交,可能读到其他事务未提交的数据,也叫作脏读
  • read commit 读已提交 ,两个读取数据的结果不一致,也叫作不可重复度,不可重复度解决了脏读的问题,他只会读取已提交的数据。

    用户开启事务读取id=1的用户,查询到age=10,在次读取发现结果=20,在同一个事务里同一个查询取到不同的结果叫不可重复度。
    
  • repeatable read 可重复读,这是mysql的默认级别,就是每次读取结果都一样,但是可能产生幻读。

  • serializable 串行,一般不会使用,他会给你每一行读取数据加锁,会导致大量超时和锁竞争问题。