Java高级面试-面经

写在前面

根据笔者面试经验总结出来的一些知识点。mark一下,希望能给大家帮助。到高级了,需要所有的知识点都成体系,零散的知识点很容易让面试官抓到薄弱处,给与降维打击。所以,我也尽量按照知识体系模块来划分。

面试java,肯定少不了jvm原理。此外关系型数据库,非关系型数据库,分布式缓存,分布式锁;高并发,

JVM原理相关

内存模型

内存模块
1、程序计数器:字符码行号指示器。
2、虚拟机栈:java方法执行的内存模型,存储局部变量,操作栈,动态栈,方法出口灯信息。使用连续的内存空间
3、java堆:保存所有对象实例,包括数组。数组也需要在堆上分配。可使用不连续的内存空间。
4、方法区:jdk1.8中,该区域已彻底移除,取而代之的是元空间。元空间不受jvm内存约束,直接在本地内存中申请内存分配。可使用不连续的内存空间。
5、运行时常量池:方法区的一部分,1.8后该区域转移到Heap堆中。

Java内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。
比如volatile,synchronize,final等。

volatile三大特性:原子性,可见性和有序性

synchronize关键字:保证同一时间只有一个线程在调度,同样保证了原子性,可见性和有序性。

可见性是基于CPU和线程之间的缓存一致性原理来说的。

GC原理

对于线程独享的内存区域,生命周期与线程相同,且该区域内对象在生成时需要明确其生命周期。
对于线程共享的区域(方法区,Heap堆),生命周期与虚拟机相同。该区域内对象没有明确的死亡时间,所以GC的主要场所就在于Heap堆。

线程生命周期
在这里插入图片描述
几种GC模式:

  1. Serial:最早的GC收集器,单线程,停顿时间长
    Serial:新生代,复制删除
    Serial old:老年代,标记清理
  2. Parallel scavenge:多线程同时执行,注重吞吐量,可设置GC的自适应策略。虚拟机自适应设置GC参数,包括老年代与新生代大小比例等。
    Parallel scavenge 配合Parallel old实现高吞吐量。
  3. ParNew:新生代收集器,多线程,其余和Serial类似
    特点:配合CMS使用。
  4. CMS:并发标记扫描,标记清除。
    特点:兵法标记,GC线程和用户线程可同时运行,大大减少了停顿时间,平且在CPU数较多是,性能提升明显。
    缺点:占用CPU时间,降低了吞吐量;产生不连续空间;产生Concurrent Mode Fail异常。

Concurrent Mode Fail:CMS GC的过程中同时业务线程将对象放入老年代,而此时老年代空间不足,或者在做Minor GC的时候,新生代Survivor空间放不下,需要放入老年代,而老年代也放不下而产生的。产生Concurrent Mode Failure的时候,收集器会降级成Serial Old收集器,停顿业务线程,等待GC收集完成。这将导致停顿时间增加,降低性能。通过调低CMSInitiatingOccupancyFraction参数,降低触发CMS的老年代负载因子,让CMS提前进入GC收集,确保分配资源时有足够的空间。

产生不连续空间:可以通过这是以下两个参数控制
-XX:UseCMSCompactAtFullCollection:在full GC钱,使用标记-整理算法
-XX:CMSFullGCBeforeCompaction=5:执行多少次非整理的Full GC后,进行一次压缩full GC。默认值为0,即每次Full GC都执行压缩full GC,整理内存碎片。

多线程

线程池中的几个重要的参数
corePoolSize :核心线程数量
maximumPoolSize :线程最大线程数
workQueue :阻塞队列,存储等待执行的任务 很重要 会对线程池运行产生重大影响
keepAliveTime :线程没有任务时最多保持多久时间终止
unit :keepAliveTime的时间单位
threadFactory :线程工厂,用来创建线程
rejectHandler :当拒绝处理任务时的策略

线程池处理流程

阻塞队列:

  • ArrayBlockingQueue:一种基于数组结构的有界队列。先进先出原则。
  • LinkedBlockingQueue:基于链表的无界组赛队列。先进先出原则,吞吐量高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用这个队列。
  • SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须零一个线程调用移除操作。吞吐量高于LinkedBlockingQueue。Executors.newCachedThreadPool()使用这个队列。
  • PriorityBlockingQueue:具有优先级的无界阻塞队列

饱和策略(Rejected Execution Handler)

  • AbortPolicy:直接抛出异常(默认策略)
  • CallerRunsPolicy:只调用所有线程来运行任务
  • DiscardOldestPolicy:丢弃最老任务,加入该任务
  • DiscardPolicy:丢弃当前任务

Executor框架成员-ThreadPoolExecutor

  • FixedThreadPool:为了满足资源管理的需求,而需要限制当前线程数量的应用场景。适用于负载比较重的服务器。
  • SingleThreadPool:适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景
  • CachedThreadPool:无界线程池,适用于执行很多的短期异步的小程序,或者负载较轻的服务器

关闭线程

  • SHUTDOWN:将线程池状态设置为shutdown状态,然后所有没有正在执行任务的线程。正在执行任务的线程,会在任务执行完后终止。
  • SHUTDOWNNOW:首先将线程池状态设置为STOP,然后尝试停止所有正在执行获暂停的线程。立即中断。

原理:均为便利线程池中的工作线程,并逐个调用线程的interrupt方法来终端。

只要调用其中一个方法,isShutdown方法就会返回true,所有任务都完成关闭,才表示线程池关闭成功,isTerminad方法才会返回true。

类加载过程

在Java中,类装载器把一个类装入Java虚拟机中,要经过三个步骤来完成:装载、链接和初始化,其中链接又可以分成校验、准备和解析三步,除了解析外,其它步骤是严格按照顺序完成的,各个步骤的主要工作如下:

  • 装载:查找和导入类或接口的二进制数据;
  • 链接:执行下面的校验、准备和解析步骤,其中解析步骤是可以选择的;
  • 校验:检查导入类或接口的二进制数据的正确性;
  • 准备:给类的静态变量分配并初始化存储空间;
  • 解析:将符号引用转成直接引用;
  • 初始化:激活类的静态变量的初始化Java代码和静态Java代码块。

Class.forName 和 ClassLoader.loadClass 的区别
Class.forName(className)方法,其实调用的方法是Class.forName(className,true,classloader);注意看第2个boolean参数,它表示的意思,在loadClass后必须初始化。比较下我们前面准备jvm加载类的知识,我们可以清晰的看到在执行过此方法后,目标对象的 static块代码已经被执行,static参数也已经被初始化。

再看ClassLoader.loadClass(className)方法,其实他调用的方法是ClassLoader.loadClass(className,false);还是注意看第2个 boolean参数,该参数表示目标对象被装载后不进行链接,这就意味这不会去执行该类静态块中间的内容。

双亲委派模式

Synchronized与lock方法

synchronized:互斥,可重入,不可中断,非公平的隐式锁实现

synchroniz关键字在普通方法和静态方法上的区别:

synchronized修饰不加static的方法,锁是加在单个对象上,不同的对象没有竞争关系;

synchronized修饰加了static的方法,锁是加载类上,这个类所有的对象竞争一把锁。

区别

  • Lock是一个接口,是JDK层面的实现,而Synchronized是java中的关键字,是Java的内置特性,是jvm层面的实现。
  • synchronized在发生异常时,会自动释放线程占有的锁,从而避免死锁。lock发生异常是,需要显式的调用unlock方法释放锁。例如在finally块中释放锁,从而避免死锁。
  • lock可以让等待的线程响应中断,而synchronized不能响应中断。
  • lock可以判断是否成功获取锁,而synchronized不能
  • lock可以提高多个线程的读操作效率。

ReentrantLock
lock锁中比较常见的就是ReentrantLock,可重入锁。

锁优化
针对synchronized关键字,jvm做了一系列锁优化,使得synchronized的加锁效率和lock相当。
锁优化的策略包括:

  • 自旋锁/适应性自旋锁
  • 消除锁
  • 锁粗化
  • 轻量级锁
  • 偏向锁

另外,对加锁的过程也做了优化。
对资源加锁时,默认加偏向锁,即一个乐观锁。认为当前资源不存在争抢,线程获取锁后,一直持有该锁,直到有其他线程申请获取该锁。
当有线程争抢的时候,终止偏向锁,膨胀为轻量级锁。采用CAS方式置换锁资源持有者的标志位(标志位记录线程ID)。采用CAS获取锁资源,而不是挂起线程,这里其实就是自旋锁。自旋一段时间仍然获取不到锁资源,也会膨胀为重量级锁。这个时间是系统设置的,也可以让锁来根据每次从开始自旋到获取到锁资源的时间,来自适应设置这个等待时间。这时候,就是一个自适应自旋锁。
当两个以上的线程同时争抢锁资源的时候,锁会膨胀为重量级锁。争抢的线程进入阻塞队列,并挂起。

jdk代理

1、原理

jdk静态代理实现比较简单,一般是直接代理对象直接包装了被代理对象。

jdk动态代理是接口代理,被代理类A需要实现业务接口,业务代理类B需要实现InvocationHandler接口。jdk动态代理会根据被代理对象生成一个继承了Proxy类,并实现了该业务接口的jdk代理类,该类的字节码会被传进去的ClassLoader加载,创建了jdk代理对象实例,

jdk代理对象实例在创建时,业务代理对象实例会被赋值给Proxy类,jdk代理对象实例也就有了业务代理对象实例,同时jdk代理对象实例通过反射根据被代理类的业务方法创建了相应的Method对象m(可能有多个)。当jdk代理对象实例调用业务方法,如proxy.addUser();这个会先把对应的m对象作为参数传给invoke()方法(就是invoke方法的第二个参数),调用了jdk代理对象实例的invoke()回调方法,在invoke方法里面再通过反射来调用被代理对象的因为方法,即result = method.invoke(target, args);。

cglib动态代理是继承代理,通过ASM字节码框架修改字节码生成新的子类,重写并增强方法的功能。

2、优缺点
jdk静态代理类只能为一个被代理类服务,如果需要代理的类比较多,那么会产生过多的代理类。jdk静态代理在编译时产生class文件,运行时无需产生,可直接使用,效率好。
jdk动态代理必须实现接口,通过反射来动态代理方法,消耗系统性能。但是无需产生过多的代理类,避免了重复代码的产生,系统更加灵活。
cglib动态代理无需实现接口,通过生成子类字节码来实现,比反射快一点,没有性能问题。但是由于cglib会继承被代理类,需要重写被代理方法,所以被代理类不能是final类,被代理方法不能是final。

因此,cglib的应用更加广泛一点。

典型数据结构

ThreadLocal数据结构

jdk1.8之后,hashMap的改进之处
1,节点table的定位,废弃了原有的indexfor方法,使用key的hash值和长度-1的二进制数做按位与运算,提高了定位的效率。采用这种方式,同样的hash值,在resize之后,位置可能不变,或者位置变为当前位置加长度的一半。hashmap扩容时,默认扩容为当前的两倍。
2,在解决hash冲突的时候,1.8之前,采用的是链表形式。链表形式的查询效率是O(n)。在n变大时,查询效率降低。因此在1.8之后,当链表元素超过TREEIFY_THRESHOLD时,会裂变成红黑树。红黑树的查询复杂度为O(logn),在n增大时,效率提升显著。
当n>TREEIFY_THRESHOLD(默认8)时,链表=》红黑树
当n<UNTREEIFY_THRESHOLD(默认6)时,红黑树=》链表
3,jdk1.8中用Node替代了Entry。
static class Node<K,V> implements Map.Entry<K,V> {}
3,concurrentHashMap除了上述改进外,废弃了原来分区加锁的概念,采用了CAS非阻塞的形式,保证了数据一致性。

sleep wait的区别

java IO模型

IO分类

IO数据结构划分

  • 按数据流向:输入流,输出流
  • 按传输单位:字节流,字符流
  • 按角色:结点流,处理流(常见的读写流都是结点流,处理流用的最多的就是带缓存的BufferedInputStream等)

四大基类

根据流的流向以及操作的数据单元不同,将流分为了四种类型,每种类型对应一种抽象基类。这四种抽象基类分别为:InputStream,Reader,OutputStream以及Writer。

问题排查

内存泄漏如何排查
1,系统出现运行缓慢,或者爆出OOM异常,先分析是否是因为内存泄漏引起。jvm堆大小设置不合理,过小的时候,也会因为无法给大对象分配资源而爆出OOM;或者因为年轻代分配不了对象,导致短暂存在的大对象分配到老年代,带来频繁full GC也是运行缓慢的原因。所以首先确认,是不是因为出现了OOM。
2,如果确定是出现了内存泄漏,导致了内存溢出,参照一下步骤排查。
a,登录Linux系统,使用jps或者ps命令,获取正在执行的jvm的线程id,即PID

使用jps:jps -l
使用ps:ps aux | grep java

b,使用jstate命令查看jvm线程的状态。主要是GC状态,以及各个内存模块的占用比例。

jstat -gcutil 20954 1000 # 每1000毫秒查看一次线程20954的gc信息

c,如果确实是内存模块在短时间内达到饱和,并且居高不下,那基本上就可以确定是内存泄漏了。
d,使用jmap命令,转储堆栈信息,分析内存

jmap -histo:live 3514 | head -7

e,使用分析工具查看堆栈信息。可使用jdk自带的visualVM分析dump文件。该工具在jkd的bin目录下。

CPU过载,如何排查
1,找到最耗CPU的进程

  • 执行top -c ,显示进程运行信息列表
  • 键入P (大写p),进程按照CPU使用率排序

2,找到最耗CPU的线程(假设1中查到的进程pid为10765 )

  • top -Hp 10765 ,显示一个进程的线程运行信息列表
  • 键入P (大写p),线程按照CPU使用率排序

3,将线程PID转化为16进制
假设2中查到的线程id为10804,其十六进制为2a34。
之所以要转化为16进制,是因为堆栈里,线程id是用16进制表示的。

4,查看堆栈信息
jstack 10765 | grep ‘0x2a34’ -C5 --color
通过打印并过滤线程的堆栈信息,就可以看到线程在执行的任务,然后具体分析代码。

MySQL数据库

存储模式InnoDB和MyISAM

项目InnoDBMyISAM
存储形式table.frm 表结构
table.ibd 数据和索引
table.frm 表结构
table.myd 数据
table.myi 索引
锁的粒度行级锁
间隙锁策略,防止幻读
MVCC控制事务,支持并发
实现四个事务隔离级别
表级锁,并发写入效率低
读操作相对快速
事务典型的事务型存储引擎不支持事务
索引B+树实现聚簇索引
主键索引的叶子节点存放数据
辅助索引的叶子节点存放主键值
B+树实现的非聚簇索引
主键索引和辅助索引的叶子节点存放的都是数据的物理存储地址
存储特点基于聚簇索引:对主键的查询具有很高的性能非聚簇索引
支持全文索引,压缩空间函数,延迟更新索引健等;表空间小,基于压缩-》性能高

由于索引结构的关系,InnoDB的索引查询,最终都是要走到主键索引。而MyISAM则不需要。查询到key对应的叶子节点后,就可以直接通过叶子节点存放的物理地址,拿到数据。

索引结构,B+树

B+数索引建立,B+树原理,

聚簇索引
索引的顺序即为数据的物理存储顺序。即搜索的叶子节点存放的是数据本身。
非聚簇索引
索引的顺序与数据的存储物理地址无关。索引的叶子节点存放的是数据指针。

索引优化

1,经常用作过滤器,或者查询频率较高的字段上建立索引
2,在sql语句中,经常进行group by或者order by的字段上建立索引
3,在取值范围较小的枚举类字段上避免建立索引,如性别
4,对于经常存取的字段避免建立索引
5,用于链接的列(主键/外键)建立索引
6,在经常查询的多个列上,建立复合索引,先后顺序按使用频率来定。

MVCC多版本控制

对表数据的修改,增加,或者删除的时候,会对根据事务id,对数据生成多个版本,并通过redo,undo日志来记录操作。在事务提交之前,对于数据表的读取,都还是取原来的版本。等到数据提交后,其他事务才能读取到提交后的数据。

事务隔离机制

MySQL数据库为我们提供的四种隔离级别:

① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。(锁表)
  ② Repeatable read (可重复读):可避免脏读、不可重复读的发生。
(mvcc每次select查询之后使用第一个查询的read view 避免读取到别人提交的数据,)
  ③ Read committed (读已提交):可避免脏读的发生,会读取到别人提交的事务数据。
(mvcc每次select同一条数据都会有一个新的read view,也就是会有多个)
  ④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。

分表分库

数据量达到一定级别之后,需要进行分表分库,以达到数据库快速响应的目标。
分表:数据纵向扩展,即按照某个既定的规则,根据主键索引来把数据映射到不同的表中。分表后,对于查询分页有一定影响。比较好的解决方案是每次查询都带上分表用的id来查询。否则,对于数据的查询需要排序和分页的时候,很难处理。有一种解决方案是,维护一个主键和搜索条件的中间表,只附带少数常用字段。在排序分页后,拿着主键id去查询详细数据。或者采用分表工具来实现。
分库:分库属于一个横向扩展,一般按照业务来划分。比如系统中商品数据量达到一定程度,可考虑分出一个商品库,然后商品分成20个子表。通过这种方式,达到商品信息查询的快速响应。

常见问题:

  1. 全局唯一id生成。
    • 自增id:通过配置每张表的起始id和自增步距,确保id唯一。缺点:分表后,难以扩展;不能保证严格递增,只能保证趋势递增。
    • 发号器服务:所有的数据库请求都要走发号器,发号器容易成为性能瓶颈。解决方案:可以批量下发。
    • UUID:全局唯一,支持广泛,简单易用。缺点:无业务含义,id随机,不能保证递增。
    • 雪花算法:在高位采用时间毫秒数,保证趋势递增。通过配置各个位置的不同含义,保证全局唯一。扩展性好。缺点:由于各机器时钟存在差异,不保证严格递增,也是趋势递增。
  2. 查询聚合问题
    • 业务层面避免跨页查询:通过要求查询必须带着分页主键来控制查询结果不跨页。
    • 技术上,维护一个分表字段映射表:例如按主键分表,每次查询分页时,先在映射表中查询到分页后的主键id,然后通过主键id去各个分表中查询具体数据。映射表字段要简单,不能过多,否则增加维护成本。缺点就是增加系统复杂度,增加维护成本。

常见组件:
简单易用的组件:

  • 当当sharding-jdbc
  • 蘑菇街TSharding

强悍重量级的中间件:

  • sharding
  • TDDL Smart Client的方式(淘宝)
  • Atlas(Qihoo 360)
  • alibaba.cobar(是阿里巴巴(B2B)部门开发)
  • MyCAT(基于阿里开源的Cobar产品而研发)
  • Oceanus(58同城数据库中间件)
  • OneProxy(支付宝首席架构师楼方鑫开发)
  • vitess(谷歌开发的数据库中间件)

预编译,防注入

通过数据库对sql语句的预编译,将传入的参数作为一个整体属性,写入到筛选的参数中,而不会被数据库解释为sql语句,从而防止sql注入。

explain查询计划

explain查询计划中,几个核心的字段含义及枚举:
1,select_type: 查询的类型

  • SIMPLE: 简单查询,不包含子查询和union
  • PRIMRARY: 包含子查询时的最外层查询; 使用union时的第一个查询
  • UNION: 包含union的查询中非第一个查询
  • DEPENDENT UNION: 与 UNION 相同,但依赖外层查询的结果
  • SUBQUERY: 子查询
  • DEPENDENT SUBQUERY: 依赖外层查询的子查询
  • DERIVED: 用于 FROM 中的子查询

2,type: 搜索数据的方法

  • null: 不需要访问索引和表即可完成, 示例: SELECT 1;
  • const: 表中仅有一行匹配,在分解查询计划时直接将其读出作为常量使用。system 是 const 类型的特例。另外,例如直接按照主键查询,也是const
  • eq_ref: 使用 PRIMARY KEY 或 UNIQUE KEY 进行关联查询。
  • ref: 使用允许重复的索引进行查询,即使用普通索引查询
  • range: 使用索引进行范围查询
  • index: 在索引上进行顺序扫描。常见于在多列索引中未使用最左列进行查询。
  • all: 扫描全表,最坏的情况

3,possible_keys: 可能使用的索引
4,key: 最终决定要使用的key
5,key_len: 查询索引使用的字节数。通常越少越好
6,ref: 查询的列或常量
7,rows: 需要扫描的行数,估计值。通常越少越好
8,extra: 额外的信息

  • using filesort: 查询时执行了排序操作而无法使用索引排序。虽然名称为’file’但操作可能是在内存中执行的,取决是否有足够的内存进行排序。应尽量避免这种filesort出现。
  • using temporary: 使用临时表存储中间结果,常见于ORDER BY和GROUP BY语句中。临时表可能在内存中也可能在硬盘中,应尽量避免这种操作出现。
  • using index: 索引中包含查询的所有列不需要查询数据表(回表)。可以加快查询速度。
  • using where: 使用了WHERE从句来限制哪些行将与下一张表匹配或者是返回给客户端
  • using index condition: 索引条件推送(MySQL 5.6 新特性),服务器层将不能直接使用索引的查询条件推送给存储引擎,从而避免在服务器层进行过滤。
  • distinct: 优化distinct操作,查询到匹配的数据后停止继续搜索

Redis相关-NoSql

redis支持的数据类型包括:字符串、列表、集合、散列表、有序集合。
在这里插入图片描述
Redis为什么这么快
1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
4、使用多路 I/O 复用模型,非阻塞 IO;
5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

redis持久化
AOF:
RDB:

redis集群部署
一致性hash算法
判定哈希算法好坏的四个定义
1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布在所有的缓冲(Cache)中去,这样可以使得所有的缓冲空间得到利用。很多哈希算法都能够满足这一条件。
2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应该能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会映射到旧的缓冲集合中的其他缓冲区。
3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上去,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应该能够尽量避免不一致的情况发生,也就是尽量降低分散性。
4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射到不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

一致性hash使用 hashcode & (len - ) 按位与的计算来快速定位节点位置。通过这个方式取代hashCode对len取模的方式,使得hash算法满足单调性,同时降低负载。

一致性hash算法
在这里插入图片描述

redis的分布式锁实现
何为分布式锁?
分布式锁如何实现?基于redis
详情参照:
分布式锁之Redis实现

redis实现分布式锁,当获取锁失败时如何处理。
1,首先判断处理任务的时耗,如果是短暂任务,比如50ms内能完成,那可以考虑自旋等待。让当前线程等待50ms后再次请求锁。
2,可以设置一个阈值,自旋次数达到阈值后仍然没有获取到锁,可以根据任务类型来采用不同策略处理。例如,如果是查询任务,则可以直接丢弃任务,然后抛出异常。系统查询失败后,重新查询即可。如果是数据处理任务,则可以建立阻塞队列来存储任务。
3,阻塞队列的建立有很多中形式,如果采用redis作为分布式锁,则可以直接在redis中创建阻塞队列,讲任务序列化后存储在redis队列中。数据类型可选list来实现。

Spring框架

Ioc与DI

参照文章:
深入讲讲spring的IoC的实现方式

AOP编程

AOP的几个核心概念

  1. 横切关注点

软件系统,可看作由一组关注点组成。其中,直接的业务关注点,是核心关注点。而为核心关注点提供服务的,就是横切关注点。一般情况下,切面中封装的方法都是横切关注点。

  1. 切面(aspect)

封装各种横切关注点的类,就是切面。

  1. 连接点(joinpoint)

所谓连接点是指那些被拦截到的点。就是spring配置文件中的切入点表达式中需要拦截的所有的方法。

  1. 切入点(pointcut)

所谓切入点是指我们要对那些joinpoint进行拦截的定义。
在类里面可以有很多方法被增强,比如实际操作中,需要增强类里面的add方法和update方法,那么当前被增强的方法称为切入点。

  1. 通知(advice)

所谓通知是指拦截到joinpoint之后所要做的事情(增强的代码)就是通知。通知分为前置通知、后置通知、异常通知、最终通知、环绕通知。
@Before前置通知:在方法执行前执行
@After后置通知:在方法执行后执行
@After-throwing异常通知:在执行核心关注点过程中,如果抛出异常则会执行
@After-returning返回通知:在后置之前执行(如果出现异常,不执行)–在方法正常执行通过之后执行的通知
@Around环绕通知:在方法之前和之后执行

6、目标对象(Target)

代理的目标对象,即增强方法所在的类。

7、织入(weave)

将切面应用到目标对象并导致代理对象创建的过程或者说把增强用到类的过程

8、引入(introduction)

一般不适用
在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段

切点pointCut定义

切点表达式
切点的功能是指出切面的通知应该从哪里织入应用的执行流。切面只能织入公共方法。

在Spring AOP中,使用AspectJ的切点表达式语言定义切点其中excecution()是最重要的描述符,其它描述符用于辅助excecution()。

excecution()的语法如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)

这个语法看似复杂,但是我们逐个分解一下,其实就是描述了一个方法的特征:

问号表示可选项,即可以不指定。

excecution(* com.tianmaying.service.BlogService.updateBlog(…))

modifier-pattern:表示方法的修饰符
ret-type-pattern:表示方法的返回值
declaring-type-pattern:表示方法所在的类的路径
name-pattern:表示方法名
param-pattern:表示方法的参数
throws-pattern:表示方法抛出的异常

在切点中引用Bean
Spring还提供了一个bean()描述符,用于在切点表达式中引用Spring Beans。例如:

excecution(* com.tianmaying.service.BlogService.updateBlog(…)) and bean(‘tianmayingBlog’)
这表示将切面应用于BlogService的updateBlog方法上,但是仅限于ID为tianmayingBlog的Bean。

也可以排除特定的Bean:

excecution(* com.tianmaying.service.BlogService.updateBlog(…)) and !bean(‘tianmayingBlog’)

自定义注解

自定义一个注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Super {
}

参数说明
@Target是用来标记作用目标

@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包

@Retention表示注解的保留位置

RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。
RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。
RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。

@Document:说明该注解将被包含在javadoc中
@Inherited:说明子类可以继承父类中的该注解

常用的注入方式

构造器注入
setter方法注入

  • byName
  • byType

接口注入

实现方式
@Resouce
@Inject
@Autowired
xml

Spring如何处理循环引用

spring通过默认使用单例模式和setter方法注入来避免循环引用出错。使用单例模式,在下次引用时,可以返回之前创建的实例。使用setter方法注入,可以先创建出实例,然后再进行属性的注入。

Spring事务传播机制

spring在TransactionDefinition接口中定义了七个事务传播行为:

  • propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中。这是最常见的选择,也是spring的默认级别。
  • propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
  • propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。
  • propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。
  • propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。
  • propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作

spring事务失效场景:

  1. 首先使用如下代码 确认你的bean 是代理对象吗?
    必须是Spring定义(通过XML或注解定义都可以)的Bean才接受事务。直接new出来的对象添加事务是不起作用的。

  2. 如使用mysql且引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB;

  3. @Transactional 注解只能应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错,事务也会失效。这一点由Spring的AOP特性决定的;

  4. 如果调用的方法没加@Transactional,那么被调用的方法家了@Transactional,也不会回滚。自调用问题,自身调用内部方法时,默认编译成this.method(),而不会重新生成代理类,自然不会走事务代理。

解决方案:
一局发生场景,选择对应的解决方案

spring bean的生命周期

生命周期如下:

  • 实例化bean对象(通过构造方法或者工厂方法)
  • 设置对象属性(setter等)(依赖注入)
  • 完成配置
    • 如果Bean实现了BeanNameAware接口,工厂调用Bean的setBeanName()方法传递Bean的ID。(和下面的一条均属于检查Aware接口)
    • 如果Bean实现了BeanFactoryAware接口,工厂调用setBeanFactory()方法传入工厂自身
  • 初始化
    • 将Bean实例传递给Bean的前置处理器的postProcessBeforeInitialization(Object bean, String beanname)方法
    • 调用Bean的初始化方法
    • 将Bean实例传递给Bean的后置处理器的postProcessAfterInitialization(Object bean, String beanname)方法
  • 使用Bean
  • 容器关闭之前,调用Bean的销毁方法

开源框架

RocketMq

RocketMQ在面试中那些常见问题及答案+汇总

Dubbo

Dubbo常见面试题
Dubbo源码环境搭建

负载均衡策略:
1,随机策略
2,轮询策略
3,最少使用策略
4,一致性hash策略

Dubbo默认选择hession作为序列化工具,Netty作为传输工具
常用的序列化方式有:
Dubbo序列化:高效二进制序列化,不成熟,不建议生产使用
hession2:高效二进制序列化,且序列化后的数据较小,适合传输
json:文本序列化,序列化之后,还要转换成二进制流来传输,性能不好
Java序列化:java自带的序列化工具,性能不理想。

关于hession序列化

  • 静态属性不能被序列化;
  • transient关键字修饰的属性不能被序列化;
  • 父类和子类中不能有相同名称的属性,否则序列化时父类属性会覆盖子类属性。读取时按子类属性读取。

Dubbo 服务注册与暴露
Dubbo(二十二)–dubbo-原理-服务暴露流程
通过ServiceBean来实现服务的注册与暴露。ServiceBean实现了两个重要的机制:
InitializingBean 和监听ContextRefreshedEvent事件。
InitializingBean:当组件创建完对象以后会调用InitializingBean的唯一的方法afterPropertiesSet,也就是在属性设置完以后来回调这个方法。afterPropertiesSet就是把spring配置文件中dubbo的标签内容保存起来。保存在ServiceBean里面。
监听ContextRefreshedEvent事件:当我们ioc容器整个刷新完成,也就是ioc容器里面所有对象都创建完成以后来回调方法onApplicationEvent(ContextRefreshedEvent event)。在这个方法里面会把上一个机制中获取到的dubbo配置包括地址,端口,服务名称等信息组成url,暴露给注册中心。同时会在底层拉起一个netty服务器,监听本机的相应端口。在暴露服务的时候,要获取到invoker(下图getInvoker()),再用exporter来暴露执行器。Exporter会用两个,dubboExporter和registryExporter。DubboExporter来开启netty服务器,registryExporter用来注册,服务(执行器)和对应的url地址,注册到注册表里。

MyBatis

MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

处理流程

在这里插入图片描述

  • 第一步通过SqlSessionFactoryBuilder创建SqlSessionFactory
  • 第二步通过SqlSessionFactory创建SqlSession
  • 第三步通过SqlSession拿到Mapper对象的代理
  • 第四步通过MapperProxy调用Maper中相应的方法,操作数据库

mybatis 一二级缓存

一级缓存
Mybatis对缓存提供支持,但是在没有配置的默认情况下,它只开启一级缓存,一级缓存只是相对于同一个SqlSession而言。所以在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。
一级缓存生命周期和sqlsession一致。随SqlSession的创建而创建,随SqlSession的销毁而销毁。SqlSession的任何insert,update,delete操作都会清空SqlSession中的一级缓存。只是清空数据,对象仍然存在,没有被销毁。

二级缓存
MyBatis的二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能。SqlSessionFactory层面上的二级缓存默认是不开启的,二级缓存的开启需要进行配置,实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的。 也就是要求实现Serializable接口,配置方法很简单,只需要在映射XML文件配置就可以开启缓存了。

1,在mapper文件中开启本mapper范围内的二级缓存

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yihaomen.mybatis.dao.StudentMapper">
    <!--开启本mapper的namespace下的二级缓存-->
    <!--
        eviction:代表的是缓存回收策略,目前MyBatis提供以下策略。
        (1) LRU,最近最少使用的,一处最长时间不用的对象
        (2) FIFO,先进先出,按对象进入缓存的顺序来移除他们
        (3) SOFT,软引用,移除基于垃圾回收器状态和软引用规则的对象
        (4) WEAK,弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象。这里采用的是LRU,
                移除最长时间不用的对形象

        flushInterval:刷新间隔时间,单位为毫秒,这里配置的是100秒刷新,如果你不配置它,那么当
        SQL被执行的时候才会去刷新缓存。

        size:引用数目,一个正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。
        这里配置的是1024个对象

        readOnly:只读,意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有
        办法修改缓存,他的默认值是false,不允许我们修改
    -->
    <cache eviction="LRU" flushInterval="100000" readOnly="true" size="1024"/>

</mapper>

2,在配置文件中,开启全局的二级缓存

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!--这个配置使全局的映射器(二级缓存)启用或禁用缓存-->
        <setting name="cacheEnabled" value="true" />
        .....
    </settings>
    ....
</configuration>

关于二级缓存:

  • 映射语句文件中的所有select语句将会被缓存。
  • 映射语句文件中的所欲insert、update和delete语句会刷新缓存。
  • 缓存会使用默认的Least Recently Used(LRU,最近最少使用的)算法来收回。
  • 缓存会被视为是read/write(可读/可写)的缓存,意味着对象检索不是共享的,而且可以安全的被调用者修改,不干扰其他调用者或线程所做的潜在修改。

二级缓存之所以要求所有的返回pojo都是可序列化的,是因为二级缓存的存储媒介不限于系统内存,也可以是memcached,OsCached等三方缓存库。如果是基于内存的二级缓存,可以不用实现Serializable。

通信相关

HTTP协议

三次握手,四次挥手

HTTPS加密协议

SSL , TLS

公共知识模块

设计模式的六大原则

设计模式中心思想:高内聚、低耦合
一,单一职责原则
单一职责原则(Single Responsibility Principle, SRP):一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
二,开闭原则
开闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
三,里氏替换原则
里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
四,依赖倒置原则
依赖倒转原则(Dependency Inversion Principle, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
五,接口隔离原则
接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
六,迪米特法则
迪米特法则(Law of Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用。迪米特法则又称为最少知识原则(LeastKnowledge Principle, LKP)。

DDD模型理论

Domain Drived Design:领域驱动设计

分布式原理 AOP和BASE

CAP理论
CAP理论作为分布式系统的基础理论,它描述的是一个分布式系统在以下三个特性中:

  • 一致性(Consistency)
  • 可用性(Availability)
  • 分区容错性(Partition tolerance)

实际应用中,系统最多能满足其中两个,无法做到兼顾。

BASE理论
分布式系统规范之BASE理论

IO分类与原理

IO分类

IO模型可分为五种:

  • blocking IO:阻塞型IO
    请求发出后,在请求得到响应前,一直阻塞,直到得到响应,然后继续完成后续操作。
  • nonblocking IO:非阻塞型IO
    请求发出后,线程继续处理后续命令。然后每隔一段时间轮询数据是否处理好。
  • IO multiplexing:多路复用型IO,也称事件驱动IO(event driven IO)
    多路复用,则是阻塞队列的复数版。请求发送完后,同时轮询多路IO,看是否有数据准备好。
  • signal driven IO:信号驱动型IO
    型号驱动型,也属于非阻塞IO,但是不是每隔一段时间询问数据是否准备好,而是数据准备好时会给线程发送信号,通知线程来处理数据。
  • asynchronous IO:异步IO

IO模型参照
五种IO模型(详解+形象例子说明)

雪花算法:

SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id,生成该id时,给每一位上赋予一定的含义,从而生成各个机器都不重复的全局唯一id。在分布式系统中的应用十分广泛,且ID 引入了时间戳。
雪花算法的原理和实现Java

zookeeper

zookeeper保证数据一致性

Paxos算法
应用程序连接到任意一台服务器后提起状态修改请求(也可以是获得某个状态所的请求),会将这个请求发送给集群中其他服务器进行表决。如果某个服务器同时收到了另一个应用程序同样的修改请求,它可能会拒绝服务器1的表决,并且自己也发起一个同样的表决请求,那么其他服务器就会根据时间戳和服务器排序进行表决。

ZooKeeper Atomic Broadcast
又称ZAB协议,简化版的Paxos算法,zookeeper自己实现的保持数据一致性的协议。这个协议保证的是最终一致性。也就是说,zookeeper满足CAP理论中的A(可用)和P(分区容错)

消息广播具体步骤

1)客户端发起一个写操作请求。
2)Leader 服务器将客户端的请求转化为事务 Proposal 提案,同时为每个 Proposal 分配一个全局的ID,即zxid。
3)Leader 服务器为每个 Follower 服务器分配一个单独的队列,然后将需要广播的 Proposal 依次放到队列中取,并且根据 FIFO 策略进行消息发送。
4)Follower 接收到 Proposal 后,会首先将其以事务日志的方式写入本地磁盘中,写入成功后向 Leader 反馈一个 Ack 响应消息。
5)Leader 接收到超过半数以上 Follower 的 Ack 响应消息后,即认为消息发送成功,可以发送 commit 消息。
6)Leader 向所有 Follower 广播 commit 消息,同时自身也会完成事务提交。Follower 接收到 commit 消息后,会将上一条事务提交。

doc容器化部署

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值