目录
- 1.面向对象三大特性
- 2.重写和重载
- 3.protected 关键字和 default 关键字的作用范围
- 4.栈帧中有哪些东西?
- 5.堆中有哪些区域?
- 6.new 一个对象存放在哪里?
- 7.CMS 收集器回收阶段
- 8.CMS 收集器回收过程哪些需要暂停线程?
- 9.HashMap JDK 1.7 和 1.8 区别
- 10.ConcurrentHashMap JDK 1.7 和 1.8 区别
- 11.进程和线程的区别
- 12.synchronized 关键字和 ReentrantLock 区别
- 13.synchronized 关键字和 ReentrantLock 实现线程安全的底层
- 14.线程池参数
- 15.Redis 的 Zset 底层数据结构
- 16.Redis 集群环境下怎么实现高可用?
1.面向对象三大特性
(1)继承:继承是从已有类得到继承信息创建新类的过程
(2)封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。
(3)多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。
你是怎样理解多态的?什么地方用过?
同一个行为具有多个不同表现形式或形态的能力。
父类引用指向子类对象,例如 List< String > list = new ArrayList< String >();就是典型的一种多态的体现形式。
2.重写和重载
1、重载发生在本类,重写发生在父类与子类之间;
2、重载的方法名必须相同,重写的方法名相同且返回值类型必须相同;
3、重载的参数列表不同,重写的参数列表必须相同。
4、重写的访问权限不能比父类中被重写的方法的访问权限更低。
5、构造方法不能被重写
3.protected 关键字和 default 关键字的作用范围
protected作用范围:不同包同一子类
default作用范围:同一包
4.栈帧中有哪些东西?
- 局部变量
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
局部变量表的容量以变量槽(Variable Slot)为最小单位,虚拟机中并没有明确指明一个Slot应占用的内存空间大小,只是很有导向性的说到每个Slot都应该能存放一个下面8种类型的其中一个。(如果是long或者double这种64位的数据类型,则需要两个Slot)
boolean,byte,char,short,int,float,reference,returnAddress
- 操作数栈
当一个方法刚执行的时候,操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中入栈和出栈。
- 动态链接
每一个栈帧都包含一个指向运行时常量池中该栈帧所属的方法的引用,持有这个引用是为了支持方法调用过程中的动态连接
- 返回地址
一个方法执行后,只有两种方法可以退出:
return,正常退出
异常,并且不在该方法中处理
- 附加信息
5.堆中有哪些区域?
-
新生代(Young Generation):新创建的对象通常会被分配到新生代中。新生代又被分为Eden区和两个Survivor区(通常是S0和S1),其中Eden区是对象最初被分配的地方,而Survivor区则是用于存储从Eden区中幸存下来的对象。
-
老年代(Old Generation):经过多次垃圾回收后仍然存活的对象会被移动到老年代中。老年代通常比新生代更大,因为Java应用程序中的大多数都是短暂的,只有少数对象会存活很长时间。
6.new 一个对象存放在哪里?
堆
7.CMS 收集器回收阶段
初始标记(STW):暂时时间非常短,标记与GC Roots直接关联的对象。
并发标记(最耗时):从GC Roots开始遍历整个对象图的过程。不会停顿用户线程
重新标记:(STW):修复并发标记环节,因为用户线程的执行,导致数据的不一致性问题
并发清理(最耗时)
收集过程
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段。
-
初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
-
并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
-
重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(比如:由不可达变为可达对象的数据),这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
-
并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的
8.CMS 收集器回收过程哪些需要暂停线程?
初始标记
重新标记
9.HashMap JDK 1.7 和 1.8 区别
HashMap1.8的底层数据结构是数组+链表+红黑树。
HashMap1.7的底层数据结构是数组加链表
JDK1.7用的是头插法,因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。
JDK1.8及之后使用的都是尾插法
10.ConcurrentHashMap JDK 1.7 和 1.8 区别
JDK1.7ConcurrentHashMap底层实现原理:
数据结构:Segment(大数组) + HashEntry(小数组) + 链表,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
JDK1.8ConcurrentHashMap底层实现原理:
数据结构:Node 数组 + 链表或红黑树,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 cas 而非 syncronized,进一步提升性能
11.进程和线程的区别
进程
是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,是系统进行『资源分配和调度』的一个独立单位。
线程
进行运算调度的最小单位,其实是进程中的一个执行任务(控制单元),负责当前进程中程序的执行
两者之间的区别
- 「本质区别」:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位
- 「在开销方面」:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小
- 「所处环境」:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
- 「内存分配方面」:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源
- 「包含关系」:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程
12.synchronized 关键字和 ReentrantLock 区别
底层实现
- synchronized是JVM层面的锁,也是Java的关键字,通过monitor对象进行完成的。ReentrantLock是JDK提供的API层面的锁
是否需要手动释放
- synchronized不需要用户去手动释放锁,ReentrantLock则需要用户去手动释放锁(lock和unlock)。
是否可中断
- synchronized是不可中断类型的锁,ReentrantLock是可以中断的
是否可以绑定多个条件
- ReentrantLock可以同时绑定多个Condition对象,只需多次调用newCondition方法即可。
- synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件。但如果要和多于一个的条件关联的时候,就不得不额外添加一个锁。
公平锁
- synchronized的锁是非公平锁,ReentrantLock默认情况下也是非公平锁,但可以通过带布尔值的构造函数要求使用公平锁。
13.synchronized 关键字和 ReentrantLock 实现线程安全的底层
synchronized
关键字和 ReentrantLock
都是 Java 中用于实现线程安全的机制,它们的底层实现原理有所不同。
synchronized 关键字是 Java 中最基本的实现线程安全的机制,它是通过 Java 虚拟机内置的监视器锁(monitor)来实现的。当一个线程进入一个被
synchronized关键字修饰的代码块时,它会尝试获取这个代码块所对应的监视器锁。如果这个锁已经被其他线程占用,那么这个线程就会被阻塞,直到这个锁被释放。当一个线程执行完一个被
synchronized` 关键字修的代码块时,它会释放这个代码块所对应的监视器锁,这样其他线程就可以获取这个锁并执行相应的代码块。
ReentrantLock
是 Java 中另一种实现线程安全的机制,它是通过 Java 提供的 Lock 接口来实现的。ReentrantLock
实现了 Lock 接口,提供了与 synchronized
关键字类似的功能,但它具有更高的灵活性和可扩展性。ReentrantLock
支持可重入锁,即同一个线程可以多次获取同一个锁,而不会被阻塞。此外,ReentrantLock
还支持公平锁和非公平锁,可以通过参数来控制锁的获取方式。
在底层实现上,ReentrantLock
使用了 CAS(Compare and Swap)操作和 AQS(AbstractQueuedSynchronizer)队列来实现锁的获取和释放。CAS 操作是一种无锁算法,可以避免锁的竞争和阻塞,提高了并发性能。AQS 队列是一个基于链表的队列,用于存储等待获取锁的线程。当一个线程试获取锁时,如果锁已经被其他线程占用,那么这个线程就会被加入到 AQS 队列中,等待锁释放。当锁被释放时,AQS 会从队列中取出一个线程,并将锁分配给它。
总之,synchronized
关键字和 ReentrantLock
都是 Java 中用于实现线程安全的机制,它们的底层实现原理有所不同。synchronized
关键字 Java 虚拟机内置的监视器锁,而 ReentrantLock
是通过 CAS 操作和 AQS 队列来实现的。在实际应用中,可以根据具体的需求和场景选择合适的机制来实现线程安全。
14.线程池参数
1、corePoolSize:线程池中的常驻核心线程数
2、maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
3、keepAliveTime:多余的空闲线程的存活时间,当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止
4、unit:keepAliveTime的单位
5、workQueue:任务队列,被提交但尚未被执行的任务
6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可
7、handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略
线程池的底层工作原理(扩展)
1.在创建了线程池后,等待提交过来的任务请求
2.当调用execute()方法添加一个请求任务时,线程池会做出如下判断
- 如果正在运行的线程池数量小于corePoolSize,那么马上创建线程运行这个任务
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
- 如果这时候队列满了,并且正在运行的线程数量还小于maximumPoolSize,那么还是创建非核心线程立刻运行这个任务;
- 如果队列满了并且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
3.当一个线程完成任务时,它会从队列中取下一个任务来执行
4.当一个线程无事可做操作一定的时间(keepAliveTime)时,线程池会判断:
- 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
- 所以线程池的所有任务完成后,它会最终收缩到corePoolSize的大小
15.Redis 的 Zset 底层数据结构
ziplist
明明有链表了,为什么出来一个压缩链表?
-
1 普通的双向链表会有两个指针,在存储数据很小的情况下,我们存储的实际数据的大小可能还没有指针占用的内存大,得不偿失。ziplist 是一个特殊的双向链表没有维护双向指针:prev next;而是存储上一个 entry的长度和 当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度更费内存。这是典型的“时间换空间”。
-
2 链表在内存中一般是不连续的,遍历相对比较慢,而ziplist可以很好的解决这个问题,普通数组的遍历是根据数组里存储的数据类型找到下一个元素的(例如int类型的数组访问下一个元素时每次只需要移动一个sizeof(int)就行),但是ziplist的每个节点的长度是可以不一样的,而我们面对不同长度的节点又不可能直接sizeof(entry),所以ziplist只好将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下一个节点。
-
3 头节点里有头节点里同时还有一个参数 len,和string类型提到的 SDS 类似,这里是用来记录链表长度的。因此获取链表长度时不用再遍历整个链表,直接拿到len值就可以了,这个时间复杂度是 O(1)
skiplist
1.是什么
跳表是可以实现二分查找的有序链表
- skiplist是一种以空间换取时间的结构。
- 由于链表,无法进行二分查找,因此借鉴数据库索引的思想,提取出链表中关键节点(索引),先在关键节点上查找,再进入下层链表查找。
- 提取多层关键节点,就形成了跳跃表
总结来讲 跳表 = 链表 + 多级索引
2.说说链表和数组的优缺点?为什么引出跳表
痛点
解决方法:升维,也叫空间换时间。
优化
跳表的时间复杂度
时间复杂度是O(logN)
跳表查询的时间复杂度分析
首先每一级索引我们提升了2倍的跨度,那就是减少了2倍的步数,所以是n/2、n/4、n/8以此类推;
第 k 级索引结点的个数就是 n/(2^k);
假设索引有 h 级, 最高的索引有2个结点;n/(2^h) = 2, 从这个公式我们可以求得 h = log2(N)-1;
所以最后得出跳表的时间复杂度是O(logN)
跳表的空间复杂度
所以空间复杂度是O(N)
跳表查询的空间复杂度分析
首先原始链表长度为n
如果索引是每2个结点有一个索引结点,每层索引的结点数:n/2, n/4, n/8 … , 8, 4, 2 以此类推;
或者所以是每3个结点有一个索引结点,每层索引的结点数:n/3, n/9, n/27 … , 9, 3, 1 以此类推;
所以空间复杂度是O(n);
3.优缺点
跳表是一个最典型的空间换时间解决方案,而且只有在数据量较大的情况下才能体现出来优势。而且应该是读多写少的情况下才能使用,所以它的适用范围应该还是比较有限的
维护成本相对要高 - 新增或者删除时需要把所有索引都更新一遍;
最后在新增和删除的过程中的更新,时间复杂度也是O(log n)
16.Redis 集群环境下怎么实现高可用?
主从复制
主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主;
哨兵模式
反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库
Cluster 集群模式
Redis 的哨兵模式基本已经可以实现高可用,读写分离 ,但是在这种模式下每台 Redis 服务器都存储相同的数据,很浪费内存,所以在 redis3.0 上加入了 Cluster 集群模式,实现了 Redis 的分布式存储,也就是说每台 Redis 节点上存储不同的内容。
在这个图中,每一个蓝色的圈都代表着一个 redis 的服务器节点。它们任何两个节点之间都是相互连通的。客户端可以与任何一个节点相连接,然后就可以访问集群中的任何一个节点。对其进行存取和其他操作。
- 集群的数据分片
Redis 集群没有使用一致性 hash,而是引入了哈希槽【hash slot】的概念。
Redis 集群有16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽。集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:
节点 A 包含 0 到 5460 号哈希槽
节点 B 包含 5461 到 10922 号哈希槽
节点 C 包含 10923 到 16383 号哈希槽
这种结构很容易添加或者删除节点。比如如果我想新添加个节点 D , 我需要从节点 A, B, C 中得部分槽到 D 上。如果我想移除节点 A ,需要将 A 中的槽移到 B 和 C 节点上,然后将没有任何槽的 A 节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。