1. 为什么集合不能放置基本数据类型
-
集合的设计初衷就是存储引用的容器,而基本类型不属于对象无法引用 自然不能处理 所以jvm才设计的基本类型对应的封装类
-
集合类有一些equals等方法,这些方法只有封装类也就是继承了Object类的类才能使用
2. 锁的降级
假设如果要做重量级锁与轻量级锁的降级 就需要做到当前执行线程需要计算等待线程状态、需要建立预判模型,需要存储升级重量级锁可以降级的重试次数,但是这样做还是没啥意义
还是看当前线程无其它等待线程释放锁 恢复锁状态 重新升级偏向锁 锁对象数据结构没存储现在是根据锁枚举动态变更锁属性的 而线程需要复制锁对象信息
3. 轻量级锁加锁过程
- 虚拟机在当前线程的栈帧中前言建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的MarkWord的拷贝
- 拷贝对象头中的MarkWord复制到锁记录中
- 拷贝成功后,虚拟机使用CAS尝试将对象的MarkWord更新指向为LockRecord的指针,并将LockRecord里的owner指针指向Object MarkWord
- 这里分两种情况:1. 更新成功 2. 更新失败
- 先说更新成功的这种情况:假设更新成功,即这个线程就有了该对象的锁,并且对象的MarkWord的锁标志位设置为“00”,即表示此对象处于轻量级锁的状态,进而进入同步快
- 更新失败:虚拟机检查对象的MarkWord是否指向当前线程的栈帧,如果不指向的话,说明多个线程竞争锁,该锁就要膨胀为重量级锁,锁标志的状态值变为”10”,MarkWord中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。而当前线便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞而采用循环去获取锁的过程。如果指向就是进入同步快。
4. 偏向锁获取过程
- 线程A到达同步快,判断是否为可偏向状态(即偏向锁的标志时都设置为1,锁标志位是否为01)
- 是:进入下一步:线程ID是否指向当前线程,不指向的话:表明不可进入偏向锁的状态,可能:1. JVM禁用了偏向锁、2. 已经进入了轻量级锁、重量级锁状态
- 检查线程ID是否指向当前线程:是则直接执行同步代码,否则通过CAS锁进行竞争。
- 竞争成功:则将MarkWord中的线程ID设置为当前线程的ID继而执行同步快代码
- 竞争失败:则表示有竞争。线程想JVM申请撤销偏向锁:当到达全局安全点时获取偏向锁的线程会被挂起,偏向锁省纪委轻量级锁没然后被阻塞在安全点的线程机组往下执行同步快代码(撤销偏向锁的时候会导致STW)之后线程会通过CAS争抢轻量级锁!
线程欲进入synchronized时,会执行以下两类操作:
- 强制写入主存储器(main memory) 当线程欲进入synchronized时,如果该线程的工作存储器(working memory)上有未映像到主存储器的拷贝,则这些内容会强制写入主存储器(store->write),则这些计算结果就会对其它线程可见(visible)。
- 工作存储器(working memory)的释放 当线程欲进入synchronized时,工作存储器上的工作拷贝会被全部丢弃。之后,欲引用主存储器上的值的线程,必定会从主存储器将值拷贝到工作拷贝(read->load)。
5. Redis不支持回滚的原因:
- Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
- 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
6. Redis WATCH命令实现乐观锁
实现原理:
被WATCH的键会被监视并发现这些键是否被改动过,假如有一个被监视的键在EXEC(事务提交)执行之前被改动那么整个事务就会被取消,EXEC返回nil-reply
来表示事务已经失败。
如果在 WATCH执行之后, EXEC执行之前, 有其他客户端修改了 mykey
的值, 那么当前客户端的事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。
7. 总结
对于一个金融系统来讲,稳定是最重要的,因为涉及到大额的资金流,求稳是这类系统的一大特色。
8. 重写OR重载
编译时多态:方法重载
运行时多态:方法重写,方法重写表现出两种多态性,**当对象引用本类实例时,为编译时多态,**否则为运行时多态。
9. 异步和非阻塞
异步调用通常需要包括一个回调机制或者事件机制,去主动通知调用方此时响应的结果已经可用了。
而非阻塞调用往往会先返回一个任意的结果,然后调用者会不定时的反复去尝试获取返回的结果,直到结果已经可用了。
相对于调用者而言,异步和非阻塞的区别就在于被调用方主动返回值OR被调用方被动返回值。
非阻塞I/O意味着当你发起一个系统调用的时候,他会立即返回一个结果,而不是将你的线程睡眠。非阻塞的读写操作,会收到一个立即的返回值,然后请求者会反复去重试,不断的去尝试,直到可以开始读写操作了。类似于忙等的状态,不断的测试,但是线程没有被阻塞。try_lock就是一个非阻塞的调用,他会尝试去获取锁,直到锁可以获取。通常来说,系统调用会进入内核,一般都是阻塞的,所以read操作往往是阻塞的,会等待可用数据,并且将线程休眠。
异步I/O
我们假设同步I/O意味着发出一个I/O命令,然后一直等待,直到I/O操作完成。也就是说,你发出一个read命令,然后这个线程接下来的执行操作会一直等待,直到已经读到了内容。异步I/O则是你发出一个I/O命令,然后这个I/O不会立即完成。你可以先去执行接下来的程序。异步会实现一个接口,允许IO操作不阻塞当前的线程,而且当操作完成之后,会主动通知你操作已经完成。
10. Finalize()的方法
这样一句话听有意思:
垃圾回收器只知道如何释放用new创建 的对象的内存,所以它不知道如何回收不是new分配的内存
10.1 工作原理
它的工作原理“假定”是这样的:当垃圾回收器准备回收对象的内存时,首先会调用其finalize() 方法,并在下一轮的垃圾回收动作发生时,才会真正回收对象占用的内存
10.2 为什么避免使用Finalize方法
finalize()方法中一般用于释放非Java 资源(如打开的文件资源、数据库连接等),或是调用非Java方法(native方法)时分配的内存(比如C语言的malloc()系列函数)。
首先,由于finalize()方法的调用时机具有不确定性,从一个对象变得不可到达开始,到finalize()方法被执行,所花费的时间这段时间是任意长的。我们并不能依赖finalize()方法能及时的回收占用的资源,可能出现的情况是在我们耗尽资源之前,gc却仍未触发,因而通常的做法是提供显示的close()方法供客户端手动调用。
11. DTO&PO&QUERY&VO
VO (View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。
DTO(Data Transfer Object):数据传输对象,这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。
**DO(Domain Object):**领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。
PO(Persistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。
12. SpringBootServletInitializer接口
首先,这个接口是javax.servlet下的。官方的解释是这样的:为了支持可以不使用web.xml。提供了ServletContainerInitializer,它可以通过SPI机制,当启动web容器的时候,会自动到添加的相应jar包下找到META-INF/services下以ServletContainerInitializer的全路径名称命名的文件,它的内容为ServletContainerInitializer实现类的全路径,将它们实例化。
13. 如何保证对象内存分配时的线程安全?
一种是对分配内存空间的动作进行同步处理–实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
另外一种是把内存分配的动作线程划分在不同的空间之中,即每个线程在Java堆中预先分配一小块内存,称之为TLAB,是本地线程分配缓冲的简写形式,那个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定操作。
14. netty如何解决粘包拆包?
14.1为什么会出现粘包拆包?
操作系统在发送TCP数据的时候,底层会有一个缓冲区,例如1024个字节大小,如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题
如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包
相反的,假设两个相邻的数据包如果数据量不大,之和未超过缓冲区,TCP就会将其沾合为一个数据包
14.2 常见的拆包粘包解决方法:
- 客户端在发送数据包的时候,每个包都固定长度,比如1024个字节大小,如果客户端发送的数据长度不足1024个字节,则通过补充空格的方式补全到指定长度;
- 客户端在每个包的末尾使用固定的分隔符,例如
\r\n
,如果一个包被拆分了,则等待下一个包发送过来之后找到其中的\r\n
,然后对其拆分后的头部部分与前一个包的剩余部分进行合并,这样就得到了一个完整的包; - 将消息分为头部和消息体,在头部中保存有当前整个消息的长度,只有在读取到足够长度的消息之后才算是读到了一个完整的消息;
- 通过自定义协议进行粘包和拆包的处理。
14.3 Netty提供的解决方案:
- FixedLengthFrameDecoder
15 如何判断jvm堆栈中一个数据类型是否为为引用类型?
需要根据GC的实现方式,来用什么样的方法,以下原文出自RednaxelaFX
- 保守式GC
- 半保守式GC
- 准确式GC
15.1 保守式GC
在进行GC的时候,JVM开始从一些已知位置(例如说JVM栈)开始扫描内存,扫描的时候每看到一个数字就看看它“像不像是一个指向GC堆中的指针”。这里会涉及上下边界检查(GC堆的上下界是已知的)、对齐检查(通常分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就肯定不是指针),以此为判断依据。
15.2 半保守式GC
JVM可以选择在栈上不记录类型信息,而在对象上记录类型信息。这样的话,扫描栈的时候仍然会跟上面说的过程一样,但扫描到GC堆内的对象时因为对象带有足够类型信息了,JVM就能够判断出在该对象内什么位置的数据是引用类型了。
15.3 准确式GC
也就是说给定某个位置上的某块数据,要能知道它的准确类型是什么,这样才可以合理地解读数据的含义;GC所关心的含义就是“这块数据是不是指针”。
要实现这样的GC,JVM就要能够判断出所有位置上的数据是不是指向GC堆里的引用,包括活动记录(栈+寄存器)里的数据。 著名的有HotSpot的OopMap。