07 08 2023

Q:谈谈你对CAS的理解?

CAS的全称是:Compare And Swap(比较再交换); 它体现的一种乐观锁的思想在无锁状态下保证线程操作数据的原子性。

CAS使用到的地方很多:AQS框架、AtomicXXX类。

在操作共享变量的时候使用的自旋锁,效率上更高一些。

CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现。

乐观锁和悲观锁的区别

CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。

synchronized 是基于悲观锁的思想: 最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。


Q:请谈谈你对 Volatile 的理解?

1. 保证线程间的可见性
用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见。

2. 禁止进行指令重排序
指令重排: 用volatile 修饰共享变量会在读、写共享变量时加入不同的屏障阻止其他读写操作越过屏障,从而达到阻止重排序的效果。


Q:Synchronized关键字的底层原理?

Synchronized[对象锁]采用互斥的方式让同一时刻至多只有一个线程能持有[对象锁]。
它的底层由monitor实现的,monitor是jvm级别的对象 (c++实现)线程获得锁需要使用对象(锁)关联monitor。

Java中的Synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高性能比较低。
轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性。
偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令。

注意: 一旦锁发生了竞争,都会升级为重量级锁。


Q:聊一下ConcurrentHashMap?

1. 底层数据结构

  • JDK1.7底层采用分段的数组+链表实现
  • JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树

2. 加锁的方式
JDK1.7:Segments数组+HashEntry数组+链表,采用Segment分段锁保证线程安全性,底层使用的是ReentrantLock。
put()操作:先根据key的hashcode的值找到对应的segment段,再根据segment中的put方法,加锁lock(),再次hash确定存放的hashEntry数组中的位置,在链表中根据hash值和equals方法进行比较,如果相同就直接覆盖,如果不同就插入在链表中。

JDK1.8:底层数据结构:Synchronized + CAS +Node +红黑树。采用CAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点。锁是锁的链表的某个节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作(因为扩容的时候使用的是Synchronized锁,锁全表)。 引入红黑树,降低了数据查询的时间复杂度。
put()操作:
1.根据key的进行hash操作,找到Node数组中的位置,如果不存在hash冲突,即该位置是null,直接用CAS插入
2.如果存在hash冲突,就先对链表的头节点或者红黑树的头节点加synchronized锁
3.如果是链表,就遍历链表,如果key相同就执行覆盖操作,如果不同就将元素插入到链表的尾部, 并且在链表长度大于8, Node数组的长度超过64时,会将链表的转化为红黑树。
4.如果是红黑树,就按照红黑树的结构进行插入。


Q:谈谈你对ThreadLocal的理解?

1. ThreadLocal可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引发的线程安全问题。
2. ThreadLocal同时实现了线程内的资源共享。
3. 每个线程内有一个ThreadLocalMap类型的成员变量,用来存储资源对象。
a)调用set方法,就是以ThreadLocal自己作为 key,资源对象作为value,放入当前线程的ThreadLocalMap集合中
b)调用get方法,就是以ThreadLocal自己作为 key,到当前线程中查找关联的资源值
c)调用remove方法,就是以ThreadLocal自己作为 key,移除当前线程关联的资源值
4. ThreadLocal内存泄漏问题
ThreadLocalMap中的key是弱引用,值为强引用; key 会被GC释放内存,关联value的内存并不会释放。建议主动remove释放 key,value。


Q:Java有几种文件拷贝方式,哪一种效率最高?

第一种,使用 java.io 包下的库,使用FileInputStream读取,再使用FileOutputStream写出。
第二种,利用 java.nio 包下的库,使用 transferTo 或 transfFrom 方法实现。
第三种,Java 标准类库本身已经提供了 Files.copy 的实现。
NIO里面提供的NIO transferTo 和 transfFrom 方法,也就是常说的零拷贝实现,它能够利用现代操作系统底层机制,避免不必要拷贝和上下文切换,因此在性能上表现比较好。


Q:简述 MyISAM 和 InnoDB 的区别?

MyISAM:
不支持事务,但是每次查询都是原子的;
支持表级锁,即每次操作是对整个表加锁;
存储表的总行数;个MYISAM表有三个文件: 索引文件、表结构文件、数据文件采用非聚集索引,索引文件的数据域存储指向数据文件的指针。辅索引与主索引基本一致,但是辅索引不用保证唯性。

InnoDB:
支持ACID的事务,支持事务的四种隔离级别:
支持行级锁及外键约束:因此可以支持写并发;
不存储总行数;
一个InnoDb引擎存储在一个文件空间(共享表空间,表大小不受操作系统控制,一个表可能分布在多个文件里),也有可能为多个(设置为独立表空,表大小受操作系统文件大小限制,一般为2G) ,受操作系统文件大小的限制;
主键索引采用聚集索引(索引的数据域存储数据文件本身),辅索引的数据域存储主键的值,因此从辅索引查找数据,需要先通过辅索引找到主键值,再访问辅索引:最好使用自增主键,防止插入数据时,为维持B+树结构,文件的大调整。


Q:微服务划分逻辑?

一个合理的服务划分应该是:符合团队结构、业务边界清晰、最小化地变更、最大化地复用、性能稳定简洁。

拆分时应该坚守哪些指导原则?
1、单一服务内部功能高内聚低耦合
也就是说每个服务只完成自己职责内的任务,对于不是自己职责的功能交给其它服务来完成。

2、闭包原则(CCP)
微服务的闭包原则就是当我们需要改变一个微服务的时候,所有依赖都在这个微服务的组件内,不需要修改其他微服务。

3、服务自治、接口隔离原则
尽量消除对其他服务的强依赖,这样可以降低沟通成本,提升服务稳定性。服务通过标准的接口隔离,隐藏内部实现细节。这使得服务可以独立开发、测试、部署、运行,以服务为单位持续交付。

4、避免环形依赖与双向依赖


Q:你们采用哪种分布式事务解决方案?

1、 Seata的XA模式,CP,需要互相等待各个分支事务提交,可以保证强一致性,性能差(银行业务)

2、 Seata的AT模式,AP,Seata默认模式,底层使用undolog 实现,性能好(互联网业务)

3、 Seata的TCC模式,AP,性能较好,不过需要人工编码实现(银行业务)

4、 MQ模式实现分布式事务,在A服务写数据的时候,需要在同一个事务内发送消息到另外一个事务,异步,性能最好


MySQL索引分类

我们可以按照四个角度来分类索引。

  • 按「数据结构」分类:B+tree索引、Hash索引。
  • 按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
  • 按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引、全文索引。
  • 按「字段个数」分类:单列索引、联合索引。

Netty 怎么解决粘包和拆包问题?

对于粘包和拆包问题,常见的解决方案有四种:

  • 客户端在发送数据包的时候,每个包都固定长度,比如1024个字节大小,如果客户端发送的数据长度不足1024个字节,则通过补充空格的方式补全到指定长度;
  • 客户端在每个包的末尾使用固定的分隔符,例如\r\n,如果一个包被拆分了,则等待下一个包发送过来之后找到其中的\r\n,然后对其拆分后的头部部分与前一个包的剩余部分进行合并,这样就得到了一个完整的包;
  • 将消息分为头部和消息体,在头部中保存有当前整个消息的长度,只有在读取到足够长度的消息之后才算是读到了一个完整的消息;
  • 通过自定义协议进行粘包和拆包的处理。

Netty提供的粘包拆包解决方案:

  • FixedLengthFrameDecoder
  • LineBasedFrameDecoder与DelimiterBasedFrameDecoder
  • LengthFieldBasedFrameDecoder与LengthFieldPrepender
  • 自定义粘包与拆包器
延伸阅读