1.类的实例化顺序
首先会加载父类,从父类的静态数据到构造函数(没有显示的调用则执行无参构造)再到非静态字段,再加载子类,子类也是静态数据->构造函数->非静态字段
2.HashMap的底层原理
HashMap的底层原理是结合了数组和链表(或红黑树)的数据结构来实现键值对的存储和快速访问。
1.'哈希表':HashMap使用哈希表来存储键值对。哈希表是一个数组,数组的每个元素是一个链表或红黑树,这些结构用于存储具有相同哈希码的键值对。
2.'哈希函数':HashMap使用键对象上的hashCode()方法来计算哈希码,然后通过哈希函数将哈希码映射到数组索引上。
3.'链表和红黑树':当多个键产生相同的哈希码时,会发生哈希冲突。HashMap通过链表法来解决这种冲突,即相同索引位置的所有键值对会以链表形式存储。在JAVA 8 中,如果链表长度达到一定阈值(默认为8),链表会被转换成红黑树,以提高查询效率。
4.'键值对节点':HashMap中的每个键值对存储在一个内部类Node<K,V>的实例中,这个类包含了四个基本属性:键、值、哈希码和下一个节点。
5.'初始容量和加载因子':初始容量是创建HashMap时内部数组的初始大小,加载因子是衡量HashMap满的程度的一个标准。当HashMap中的条目数超过了加载因子与当前容量的乘积时,HashMap会进行扩容操作(默认加载因子是0.75),扩容创建新数组,是原来数组的两倍。
6.'扩容机制':当HashMap中的元素数量达到当前容量与加载因子的乘积时,HashMap会进行扩容操作,即创建一个新的更大的数组,并将所有现有的元素重新哈希到新数组中。
7.'为什么初始容量要是2的整数次幂':这是因为HashMap使用位运算来代替模运算来计算哈希码对应的数组索引。这样做的好处是可以更快的定位数组索引,同时也减少了哈希冲突的概率。
class HashMap<K, V> {
Node<K, V>[] table;
int size;
int threshold;
flot loadFactor;
static class Node<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next;
Node(int hash, K key, V value, Node<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
}
这个结构保证了HashMap在大多数情况下都能提供时间复杂度的查询、插入和删除操作
3.ConcurrentHashMap
ConcurrentHashMap实现线程安全的核心机制是通过分段锁(Segment Locking)和更细粒度的同步控制。在JDK 1.5到JDK 7中,ConcurrentHashMap通过将内部数据分为多个段(Segment),每个段都有自己的锁,这样多个线程可以同时访问不同段的数据,从而实现并发读写。
以下是ConcurrentHashMap实现线程安全的几个关键点:
1. 分段锁(Segment Locking) :每个段(Segment)维护着一个独立的锁,这样只要多个线程访问不同的段,它们就可以并行执行,从而提高了并发性能。
1. 原子性操作:ConcurrentHashMap提供了诸如`put`、`get`、`remove`等原子性操作,这些操作内部使用了锁来保证线程安全。
1. CAS操作(Compare and Swap) :在JDK 8中,ConcurrentHashMap不再使用分段锁,而是转而使用CAS操作和其他同步机制,如`synchronized`和`volatile`关键字,来实现更高效的并发控制。
1. Node的volatile修饰:JDK 8中,ConcurrentHashMap的内部节点(Node)使用`volatile`关键字修饰,保证了节点状态的可见性,从而支持无锁读取操作。
1. 其他并发控制技巧:ConcurrentHashMap还使用了一些其他技巧,如`putAll`方法的内部实现会确保在添加多个元素时保持线程安全。
以下是JDK 8中ConcurrentHashMap的一个简化版的put操作的代码片段,展示了如何使用同步机制来实现线程安全:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
}
}
}
4.有没有有顺序的Map实现类,如果有,他们是怎么保证有序的
Java中提供了有序的Map实现类是LinkedHashMap和TreeMap
1.LinkedHashMap:
LinkedHashMap维护了一个双向链表,用来维护插入顺序或者访问顺序。
如果构造函数指定了accessOrder参数为true,那么每次访问一个键值对(通过get或者put方法),这个键值对都会被移动到链表末尾
在插入元素或者访问元素时,LinkedHashMap通过这个双向链表来维护顺序
2.TreeMap:
TreeMap是基于红黑树实现的,它维护了键的顺序关系。
键的顺序可以通过Comparator或者键实现Comparable接口来决定。
插入元素时,TreeMap会根据键的顺序关系将元素插入到合适的位置,以保持数的平衡和键的有序性。
在查找元素时,TreeMap可以通过二分查找快速定位元素,时间复杂度为O(logn)
5.having和where的区别
where过滤子句是在查询过程中对表中的数据的过滤条件,不允许使用聚合函数作为过滤条件,原因在于时机不对。聚合函数是对表中数据查询后的结果集进行统计处理的,两者的执行时机不一致。
Having过滤子句是对查询结果集进行过滤的,可以在该子句后面使用聚合函数,因为时机相同,聚合函数是处理结果集的,having子句又是过滤结果集的,可以在一起使用,另外having不能单独使用,只能跟在group by分组子句后面。
6.Mybatis是如何进行分页的?分页插件的原理是什么?
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在SQL中直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的SQL,然后重写SQL,根据dialect方言,添加对应的物理分页语句和物理分页参数。
7.操作系统中线程切换过程是怎样的?
1.保存当前线程状态:这通常涉及到保存程序计数器(存储CPU正在执行的指令位置)、寄存器文件(包含CPU使用的各种寄存器的当前值)和其他线程的特定状态信息。
2.选择下一个线程:调度器根据某种调度算法(如转轮算法、优先级算法等)来选择下一个要执行的线程。
3.加载下一个线程状态:将新选择线程的上下文信息加载到CPU中,包含程序计数器和寄存器文件等。
4.更新调度器状态:更新调度器中的信息,以反应当前正在执行的线程和系统中所有线程的当前状态。
5.切换到新线程。
8.进程和线程的区别
1.定义和组成:
进程:是操作系统进行资源分配和调度的基本单位,它是一个程序的执行实例。每个进程都有自己的地址空间、数据栈以及其他用于跟踪执行的辅助数据。
线程:是进程的执行单元,是CPU调度和分派的基本单位,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器、栈),但它可以与同属一个进程的其他线程共享进程所拥有的全部资源。
2.资源拥有:
进程:拥有独立的内存空间和其他系统资源,如打开的文件列表、网络连接等。
线程:共享进程的资源,如内存和文件句柄,但每个线程有自己的栈空间。
3.创建和管理开销:
进程:创建进程需要较多的系统资源,因为它需要独立的地址空间和资源,进程间的切换(上下文切换)开销较大。
线程:创建线程需要的资源较少,线程间的切换(上下文切换)开销较小。
4.通信方式:
进程:通常使用进程间通信(IPC)机制,如管道、消息队列、共享内存等。
线程:由于共享同一进程地址空间,通常使用全局变量、直接函数调用等方式通信。
5.执行环境:
进程:每个进程都有自己的执行环境,环境之间相互独立。
线程:线程共享进程的执行环境,包括程序计数器、寄存器集合等。
6.错误影响范围:
进程:一个进程奔溃通常不会影响其他进程。
线程:一个线程奔溃可能导致整个进程奔溃。
7.调度:
进程:操作系统调度器负责调度。
线程:通常由操作系统或线程库负责调度。
9.top 命令之后有哪些内容,有什么作用
`top` 命令是一个常用的性能监控工具,用于在类Unix操作系统中实时显示系统中运行的进程的资源使用情况。它显示的内容通常包括:
1. 系统信息:包括当前时间、系统运行时间、登录用户数以及平均负载(1分钟、5分钟、15分钟)。
2. 进程列表:默认按CPU使用率排序显示进程列表,包括以下信息:
- PID(进程ID)
- USER(运行该进程的用户)
- PR(优先级)
- NI(nice值)
- VIRT(虚拟内存使用量)
- RES(常驻内存使用量)
- SHR(共享内存使用量)
- S(进程状态,如R(运行)、S(睡眠)、D(不可中断睡眠)等)
- %CPU(CPU使用率)
- %MEM(内存使用率)
- TIME+(CPU时间,包括子进程)
- COMMAND(进程名称)
`top` 命令的作用包括:
- 实时监控系统性能,如CPU和内存使用情况。
- 查看系统中的活跃进程及其资源消耗情况。
- 动态排序进程,以便快速识别资源使用较高的进程。
- 杀死或调整优先级等操作进程。
10.cup爆高 怎么排查问题
1.定位高CPU使用的进程:
使用top命令查看系统整体CPU使用情况,找到CPU使用率最高的进程。
2.定位高CPU的线程:
使用top -Hp <pid>命令查看特定进程内各个线程的CPU使用情况。
使用大写的P将线程按照CPU使用率排序,并找到使用率最高的线程ID。
3.线程状态分析:
使用jstack <pid>命令获取进程的线程栈信息。
将占用CPU最高的线程ID转换为16进制,并在jstack的输出中搜索该线程ID,以定位线程的调用栈。
4.采样分析:
如果无法直接在服务器上运行top,可以通过间隔固定时间(如10秒)多次运行jstack命令,采样线程栈信息。
对比采样结果,找出始终处于运行状态的线程,分析问题线程。
5.排除法和监控:
如果现场丢失,可以通过排除法分析可能的原因,如突发压力、内存泄漏、IO问题或网络问题。
通过监控工具观察JVM线程数的变化,以及负载均衡器的流量日志来辅助定位问题。
6.压力测试和性能分析:
如果怀疑是压力导致的CPU使用过高,可以进行压力测试,并使用性能分析工具(如jvisualvm的Profiler功能)来定位热点方法。
7.风险评估和优化:
找到问题后,评估问题的风险,并制定临时和终极解决方案。
持续优化系统和代码,以避免类似问题的再次发生。
11.什么是Spring MVC框架的控制器
控制器提供一个访问应用程序的行为,此行为通常通过服务接口实现。控制器解析用户输入并将其转换为一个由视图呈现给用户的模型。Spring用一个非常抽象的方式实现了一个控制层,允许用户创建多种用途的控制器。
12.数据传输的事务定义有哪三种?
和MQTT的事务定义一样都是3种。
1.最多一次消息:消息不会被重复发送,最多被传输一次,但也有可能一次不传输。
2.最少一次:消息不会被漏发送,最少会被传输一次,但也有可能被重复传输。
3.精确的一次:不会漏传也不会重复传输,每个消息都传输一次而且仅仅被传输一次。
13.谈谈对Syschronized关键字、类锁、方法锁、重入锁的理解
1.Syschronized关键字:
Synchronized是Java种用于实现同步锁机制的关键字。它可以保证在同一时刻,只有一个线程可以自行某个方法或代码块。Synchronized通过锁的状态和锁的获取释放机制,实现了线程之间的同步,从而保证了操作的原子性,解决了多线程的并发问题。
2.类锁
类锁是指当锁对象是一个类(Class)对象,所有该类的实例对象都共享这把锁。当线程访问该类的synchronized静态方法或静态代码块时,会获取类锁,其他线程访问同一类的这些同步静态方法或代码块将阻塞。
3.方法锁
方法锁是指当锁对象是一个实例对象时,每个实例都有自己的锁。当线程访问该对象的synchroized实例方法,会获取对象锁,其他线程访问同一对象的这些同步实例方法时将被阻塞。
3.重入锁
重入锁是java.util.concurrent.locks包下的一个类,它实现了Lock接口。重入锁允许线程在已经获取到锁的情况下再次获取锁,即线程可以多次进入同一锁保护的代码块。重入锁比内置锁提供了更灵活的操作,如可中断的锁获取、尝试非阻塞地获取锁、支持公平锁等。
4.相同点和不同点:
Synchronized和ReentrantLock都用于实现线程同步,确保操作的互斥性。
不同点:
Synchronized是Java语言层面的关键字,而ReentrantLock是Java类库中的一个类。
Synchronized锁无法中断,而RenntrantLock提供了中断机制。
ReentrantLock可以尝试非阻塞地获取锁(tryLock),而synchronized必须阻塞直到获取到锁。
ReentrantLock支持公平锁,即按照线程请求锁的顺序来分配锁,而synchronized锁默认是非公平锁的。
5.Synchronized锁的状态
无锁状态:线程可以自由进入同步代码块。
已锁定状态:线程正在执行同步代码块,其他线程请求同一锁时将被阻塞。
等待状态:线程在等待获取锁。
14.volatile的原理
1.可见性:当变量被声明为volatile时,它保证了不同线程对这个变量的读写操作都会直接发生在主内存中,而不是线程的本地缓存。这意味着一旦一个线程修改了这个volatile变量的值,这个新值会立即对其他线程可见,其他线程读取这个变量时会直接从主内存中读取,而不是使用它们自己的缓存副本。
2.有序性:编译器和处理器可能会对指令进行重新排序以提高性能。然而,当变量声明为volatile时,JVM会在volatile变量的读写操作的前后插入内存屏障,这些屏障会禁止特定类型的处理器重新排序,确保指令的执行顺序与程序中代码的顺序一致。
以下是JVM中volatile变量的一个简化演示:
volatile int x = 1;
volatile int y = 2;
x = 5;
y = 8;
int a = y;
int b = x;
3.非原子性:虽然volatile可以保证单个读或者写操作的原子性,但它不能保证复合操作(如自增、自减或检查后执行逻辑)的原子性。
注意:volatile并不是适用于所有并发情况。在复杂的同步场景中,比如需要原子地更新共享状态时,通常需要使用锁或其他并发控制机制来保证操作的原子性和更高级别的同步保证。
15.Redis 集群方案应该怎么做?都有哪些方案?
1.主从复制(Master-Slave Replication):
这种方案包括一个主节点和多个从节点。
主节点处理写操作,而从节点负责读操作,同时也可以用作数据备份。
如果主节点故障,可以通过手动或者自动(如哨兵模式)的方式将从节点提升为新的主节点
2.哨兵模式(Sentinel):
哨兵模式是基于主从复制模式的,它提供了监控和自动故障转移的功能。
哨兵实例会监控主从节点的健康状态,并在主节点不可用时触发故障转移过程。
3.Redis Cluster:
Redis Cluster是Redis官方提供的分布式解决方案,支持数据分片。
它允许自动分割数据到多个节点,支持自动重分片和故障转移。
Redis Cluster支持多种类型的分布式操作,并且提供了高可用性和扩展性。
4.Codis:
Codis是一个由外部维护的Redis集群解决方案,它在Redis Cluster出现之前就已经被广泛使用。
它通过中间件(Proxy)来管理多个Redis实例,支持数据分片和自动故障转移。
Codis的优点是易于部署和运维,并且可以与现有的Redis环境集成。
5.双集群互为备份:
这个方案涉及到使用两个Redis集群,业务主要使用一个集群,另外一个集群作为备份。
选择Redis集群方案时,要考虑以下因素:
业务需求:数据量、读写比例、可用性和扩展性要求。
成本预算:是否愿意为高可用性和性能支付额外费用。
运维能力:是否有足够的资源和知识来维护复杂的集群环境。
16.分布式系设计会考虑哪些策略
1.数据分片和复制:为了提高性能和可用性,数据会被分割(分片)并在多个节点上存储(复制)。这有助于负载均衡和容错
2.故障转移和自动恢复:系统应该设计成宅节点或组件失败时能够自动切换到健康节点,并能够自动恢复。
3.负载均衡和容错:通过负载均衡策略,系统可以摘多个服务器之间分配请求,以提高资源利用率和系统响应能力。
4.分布式一致性协议:为了保持数据一致性,需要采用如Raft或Paxos等一致性协议。
5.监控和预警:实施全面的监控来跟踪系统性能,并在出问题时及时发出警告。
6.弹力设计:系统应该能够优雅地处理故障,通过重试、熔断、限流等机制来提高系统的稳定性和可靠性。
7.模块化和易购技术栈:将系统拆分成模块,并允许不同模块使用最适合其需求的技术栈。
8.快速交付和迭代:通过分配不同团队同时开发多个模块,加快整体交付速度。
9.架构模式的运用使用如表分片、优先级队列等模式来优化系统架构。
10.成本和性能的权衡:在系统设计时需要考虑成本和性能的平衡,确保解决方案既高效又经济。
17.怎么理解栈、堆?堆中存什么?栈中存什么?
栈(Stack):
理解:栈是一种先进先出(LIFO)的数据结构,用于存储局部变量和函数调用的上下文信息。它在函数调用和返回时自动分配和释放内存。
存储内容:栈中存储的是基本类型(如整数、浮点数、指针等),以及这些基本类型组成的固定大小的复合数据结构(如结构体、数组,但通常是较小的数组)。这些数据的大小在编译期就已经确定,并且它们的生命周期是有限的,通常与当前函数的作用域一致。
特点:栈操作非常快速,因为它的内存分配和回收是通过移动栈指针完成的,不需要复杂的内存管理。
堆(Heap):
理解:堆是程序在运行时动态分配内存的区域,它的生命周期不是由作用域来决定的,而是由创建和销毁时的代码逻辑决定。
存储内容:堆中存储的是动态大小的数据,如对象、动态分配的数组等。这些数据的大小可能在运行时发生变化,或者需要再多个函数调用或对象之间共享。
特点:堆内存的分配和回收比栈慢,因为它需要更为复杂的内存管理机制,比如垃圾回收机制(GC)或引用计数(ARC)。这增加了性能开销,但提供了更大的灵活性和更长的生命周期管理。
总结:
栈中存储的是静态的、固定大小、固定生命周期的数据
堆中存储的是动态的、不固定大小、不固定生命周期的数据
18.为什么要把栈和堆区分出来呢?不都可以存放数据吗?
堆和栈的区分是为了提供不同场景下的内存管理策略,以满足程序在不同情况下的性能和内存使用需求。