自我介绍
手撕算法:模拟计算机,输入算数表达式,输出结果
#include<bits/stdc++.h>
using namespace std;
// 判断运算符的优先级
int getPriority(char op) {
if (op == '+' || op == '-') {
return 1;
} else if (op == '*' || op == '/') {
return 2;
}
return 0;
}
// 执行运算
int calculate(int num1, int num2, char op) {
if (op == '+') {
return num1 + num2;
} else if (op == '-') {
return num1 - num2;
} else if (op == '*') {
return num1 * num2;
} else if (op == '/') {
return num1 / num2;
}
return 0;
}
// 计算表达式的值
int evaluateExpression(const std::string& expression) {
std::stack<int> operandStack;
std::stack<char> operatorStack;
for (char c : expression) {
if (isdigit(c)) {
// 如果是数字,直接入栈
operandStack.push(c - '0');
} else if (c == '(') {
// 如果是左括号,直接入栈
operatorStack.push(c);
} else if (c == ')') {
// 如果是右括号,计算括号内的表达式
while (!operatorStack.empty() && operatorStack.top() != '(') {
int num2 = operandStack.top();
operandStack.pop();
int num1 = operandStack.top();
operandStack.pop();
char op = operatorStack.top();
operatorStack.pop();
operandStack.push(calculate(num1, num2, op));
}
operatorStack.pop(); // 弹出左括号
} else if (c == '+' || c == '-' || c == '*' || c == '/') {
// 如果是运算符,判断优先级并计算
while (!operatorStack.empty() && operatorStack.top() != '(' &&
getPriority(operatorStack.top()) >= getPriority(c)) {
int num2 = operandStack.top();
operandStack.pop();
int num1 = operandStack.top();
operandStack.pop();
char op = operatorStack.top();
operatorStack.pop();
operandStack.push(calculate(num1, num2, op));
}
operatorStack.push(c);
}
}
// 计算剩余的表达式
while (!operatorStack.empty()) {
int num2 = operandStack.top();
operandStack.pop();
int num1 = operandStack.top();
operandStack.pop();
char op = operatorStack.top();
operatorStack.pop();
operandStack.push(calculate(num1, num2, op));
}
return operandStack.top();
}
int main() {
std::string expression;
std::cout << "input expression:";
std::getline(std::cin, expression);
int result = evaluateExpression(expression);
std::cout<< result << std::endl;
return 0;
}
ConcurrentHashMap底层实现
ConcurrentHashMap:JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。在JDK1.7的时候,ConcurrentHashMap 对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化)整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。
BIO,NIO,AIO三者之间的区别
BIO:同步阻塞IO,一个线程对应一个客户端连接,当有新的客户端连接时,就需要创建一个新的线程来处理,线程数量多,资源占用多,处理相对比较慢。适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中。
NIO:同步非阻塞IO,一个线程可以处理多个客户端连接,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理,处理速度相对较快。适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持。
AIO:异步非阻塞IO,AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
NIO和BIO的区别:
- BIO是面向流的,NIO是面向缓冲区的
- BIO是阻塞的,NIO是非阻塞的
- buffer和channel之间的数据流向是双向的。
epoll如何实现IO多路复用
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。因为它会复用文件描述符集合来传递结果而不是每次都传递。另外,epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
用户态和内核态的区别,为什么要分这两种状态
用户态和内核态都是操作系统中的两种特权级别或者说是运行模式,在内核态下,操作系统拥有最高的权限和访问系统资源的能力,可以执行特权指令和直接访问硬件设备,在用户态下,应用程序只能执行受限的指令集,并且不能直接访问底层系统资源,必须通过系统调用陷入到内核态才能够访问底层系统资源。
区分这两种状态的目的是为了隔离了操作系统代码(操作系统的代码要比应用程序的代码健壮很多)与应用程序代码。即便是单个应用程序出现错误也不会影响到操作系统的稳定性,这样其它的程序还可以正常的运行
NIO为什么比BIO性能好,结合用户态和内核态来回答
非阻塞:BIO是阻塞式的,即在进行I/O操作时,线程会被阻塞,直到数据准备好或者写入完成。而NIO是非阻塞的,线程在进行I/O操作时可以继续执行其他任务,不需要等待数据的准备或者写入完成。这样可以提高系统的并发性能,一个线程可以同时处理多个连接。
事件驱动:NIO使用事件驱动的方式处理I/O操作。通过Selector选择器,线程可以注册对不同I/O事件(如读、写、连接、接收等)的关注,并在事件就绪时进行相应的处理。这样可以避免了线程阻塞等待I/O操作完成的情况,提高了系统的响应速度和吞吐量。
缓冲区:NIO使用缓冲区(Buffer)来进行数据的读写操作,而BIO使用流(Stream)。缓冲区可以提供更高效的数据传输,减少了数据拷贝的次数。此外,缓冲区还可以进行随机访问,更加灵活。
零拷贝:NIO可以通过直接内存访问(Direct Buffer)实现零拷贝。在数据从磁盘或者网络中读取到内存时,可以直接将数据从内核缓冲区复制到用户空间的缓冲区,避免了数据在内核空间和用户空间之间的多次复制。这样可以减少CPU的开销,提高数据传输效率。
综上所述,NIO通过非阻塞、事件驱动、缓冲区和零拷贝等特性,结合了用户态和内核态的优势,提供了更高效的I/O操作方式,从而比BIO具有更好的性能。
进程和线程的区别
进程是资源分配的最小单位,线程是CPU调度的最小单位。一个进程可以包含多个线程,同一个进程中的多个线程共享该进程的资源。进程的崩溃,在保护模式下不会对其他进程产生影响,但是线程的崩溃会导致整个进程的崩溃。
进程间通信的方式
- 管道:管道是半双工的,数据只能在一个方向上流动,需要双方通信时,需要建立起两个管道。常见的管道有命名管道和匿名管道,命名管道可以在无亲缘关系的进程间通信,而匿名管道只能在有亲缘关系的进程间通信。
- 消息队列:消息队列是消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,实现进程对资源的互斥访问。信号量的取值大于等于0,当为0时表示资源不可用。当一个进程想要访问共享资源时,先对信号量做减1操作,如果得到的值小于0,则进程被阻塞,直到信号量大于0时再对信号量做加1操作,然后进程访问共享资源。如果得到的值大于0,则进程可以直接访问共享资源。
- 信号:信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。当一个进程对信号感兴趣时,可以通过系统调用捕获信号,当信号发生时,内核就会将信号发送给该进程,该进程接收到信号后,就会执行信号处理函数对信号进行处理。
- 共享内存:共享内存指两个或多个进程共享一个给定的存储区,一般配合信号量使用。共享内存是最快的一种IPC,因为进程是直接对内存进行存取。
MQTT协议,优势是什么,为何物联网普遍使用MQTT
MQTT是一个基于发布/订阅(publish/subscribe)模式的“轻量级”通讯协议,该协议构建于TCP/IP协议之上,由IBM在1999年发布。MQTT最大的优势是轻量级,协议简单,使用方便,可实现设备与设备之间的消息通信,同时MQTT协议也是物联网中的重要组成部分。优点是代码量少,开销低,带宽占用低,即时通信协议
l MQTT 可以被解释为一种低开销,低带宽占用的即时通讯协议,可以用较少的代码和带宽为远程设备连接提供实时可靠的消息服务,它适用于硬件性能低下的远程设备以及网络状况糟糕的环境下,因此 MQTT 协议在 IoT(Internet of things,物联网),小型设备应用,移动应用等方面有较广泛的应用。
rocketmq的内部组件有哪些
RocketMQ主要由 Producer、Name Server 、Broker、Consumer 四部分组成,其中Producer 负责生产消息,Name Server名称服务充当路由消息的提供者,Consumer 负责消费消息,Broker 负责存储消息。Broker 在实际部署过程中对应一台服务器,每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。
- Name Server:名称服务,负责维护整个消息系统中所有Topic的路由信息,当Producer发送消息时,会先从Name Server中获取到Topic的路由信息,然后根据路由信息选择要发送的Broker,Name Server还负责Broker的动态注册与发现。
- Broker:消息中转角色,负责存储消息,转发消息,当Producer发送消息时,会先从Name Server中获取到Topic的路由信息,然后根据路由信息选择要发送的Broker,Broker接收到消息后,将消息存储于磁盘,然后向Producer发送ACK响应,告诉Producer消息发送成功,Broker还负责向Consumer推送消息。
- Producer:消息生产者,负责生产消息,与 Name Server 集群中的其中一个节点(随机)建立长链接(Keep-alive),定期从 Name Server 读取 Topic 路由信息,并向提供 Topic 服务的 Master Broker 建立长链接,且定时向 Master Broker 发送心跳。
- Consumer:消息消费者,与 Name Server 集群中的其中一个节点(随机)建立长连接,定期从 Name Server 拉取 Topic 路由信息,并向提供 Topic 服务的 Master Broker、Slave Broker 建立长连接,且定时向 Master Broker、Slave Broker 发送心跳。Consumer 既可以从 Master Broker 订阅消息,也可以从 Slave Broker 订阅消息,订阅规则由 Broker 配置决定。
rocketmq保证消费有序,消费者组是如何保证有序消费的,以及不重复消费?(结合分区来回答)
rocketmq如何提高吞吐量,例如生产者生产很多消息,消费者组这边要如何消费去平衡消息的堆积,并发消费问题等?
- 增加消费者实例,增加消费者实例可以提高消费的并发度,从而提高消费的吞吐量。
- 调整负载策略,可以通过调整负载策略来提高消费的吞吐量,例如可以将消息队列分配给同一个消费者实例,这样可以避免消费者实例之间的负载均衡,提高消费的吞吐量。
TCP和UDP的区别
- tcp是面向连接的,但是udp是面向无连接的。
- 消息传输的可靠性:udp是不可靠的,tcp是可靠的。
- 是否有状态:TCP传输是有状态的,也就是说TCP会去记录自己发动的消息的状态:是否发送了,是否被接收了等,而UDP则不会管发送之后的事情了。
- 传输方式:TCP是面向字节流的,UDP是面向报文的。
- 传输效率:由于TCP传输的时候有连接,确认,重传等机制,所以传输效率比较低,而UDP则没有这些机制,所以传输效率比较高。
- TCP只支持点对点的通信,UDP支持一对一,一对多,多对一,多对多的通信。
- 首部开销:TCP的首部开销为20-60字节,UDP的首部开销为8字节。
http的报文有哪些字段
请求报文:请求行,请求头,请求体
请求行:请求方法,请求路径,HTTP版本
请求头:请求头字段名,请求头字段值
请求体:请求体内容响应报文:状态行,响应头,响应体
状态行:HTTP版本,状态码,状态码描述
响应头:响应头字段名,响应头字段值
响应体:响应体内容
http的请求方法有哪些
- GET:获取资源
- POST:提交资源
- PUT:更新资源
- Delete:删除资源
- HEAD:获取报文首部
- OPTIONS:获取支持的请求方法
它们的区别主要在于: - GET和POST的区别:GET请求的参数会附加在URL之后,以?分割URL和传输数据,参数之间以&相连,POST把提交的数据则放置在是HTTP包的包体中。
osi七层模型和tcp/ip四层模型的区别
- osi七层模型:应用层,表示层,会话层,传输层,网络层,数据链路层,物理层
应用层:为应用程序提供服务,各种应用软件包括Web应用
表示层:数据格式表示,基本压缩加密功能
会话层:控制应用程序之间的对话,如不同软件数据分发给不同软件
传输层:提供端到端的可靠报文传输和错误恢复,如TCP,UDP
网络层:为数据包选择路由,如IP,ICMP
数据链路层:传输有地址的帧以及错误检测功能,如以太网,WIFI
物理层:以二进制数据形式在物理媒体上传输数据,如网线,光纤 - tcp/ip四层模型:应用层,传输层,网络层,数据链路层
区别:七层模型是一个标准,而不是实现,四层模型是一个实现应用模型,是由七层模型简化而来的。
其中,应用层,表示层,会话层合并为应用层,物理层,数据链路层合并为网络接口层。
DNS服务在哪一层:应用层,负责域名解析为IP地址
浏览器输入URL请求时,会用到哪些协议
- DNS协议:域名解析为IP地址
- UDP协议:DNS协议使用UDP协议进行通信
- http协议:浏览器与服务器之间的通信协议,得到ip地址之后,浏览器和服务器之间会建立一个http连接
- TCP协议:http协议使用TCP协议进行通信
- IP协议:TCP协议使用IP协议进行通信
- PPP协议,SLIP协议:在一个网段内进行寻址,对应的以太网协议
- ARP协议:根据IP地址获取MAC地址
具体过程:
浏览器中输入URL后,执行的全部过程。会用到哪些协议?
docker文件系统
docker的文件系统分为镜像层和容器层,镜像层是只读的,容器层是可读可写的,当容器启动时,会在镜像层上加载一个可读可写的层,这个层就是容器层,当容器中的进程写入数据时,会将数据写入到容器层,当容器中的进程读取数据时,会先在容器层中查找,如果没有找到,再去镜像层中查找。
Springcloud和springboot的区别
- Springboot是Spring的一套快速配置的脚手架,可以基于springboot快速开发单个微服务,springcloud是基于springboot实现的云原生应用开发工具
- springboot专注于快速,方便集成单个个体,springcloud是关注全局的服务治理框架,他将springboot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理,服务发现,断路器,路由,微代理,事件总线,全局锁,决策竞选,分布式会话等等集成服务
- springboot使用约定优于配置的理念,很多集成方案已经帮你选择好了,能不配置就不配置,springcloud很大的一部分是基于springboot来实现的,springboot可以离开springcloud独立使用开发项目,但是springcloud离不开springboot,属于依赖的关系
JVM垃圾回收机制
JVM会自动回收没有被引用的对象,JVM中的垃圾回收器会定期扫描堆内存中的对象,如果发现某个对象没有被引用,就会将其回收。识别垃圾对象的算法有:引用对象计数法,可达性分析法。
引用对象计数法:它给每一个对象加一个引用计数器,每当有一个地方引用时,计数器就加一,当引用失效时,计数器就减一,任何时刻计数器为0的对象就是不可能再被使用的,可以被回收的对象。缺点:无法解决对象之间相互循环引用的问题。单独存储计数字段,增加了存储空间的开销。
可达性分析法:通过一系列的称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。缺点:算法效率不高。
但在可达性分析法中,不可达的对象并不是非死不可的,这些不可达的对象有可能在finalize()方法中复活,如果在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,这个对象就会被移出“即将回收”的集合,如果对象没有复活,就会被回收。
对应的垃圾回收算法:
- 标记-清除算法:标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。缺点是效率不高,会产生大量不连续的内存碎片。
- 复制算法:将内存分为大小相等的两块,每次只使用其中一块,当这一块内存用完了,就将还存活的对象复制到另一块上面,然后再把已使用的内存空间一次清理掉。缺点是内存缩小为原来的一半。
- 标记整理法:标记出所有需要回收的对象,在标记完成后,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。缺点是效率不高。
- 分代收集算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代中的对象大多是朝生夕死的,老年代中的对象大多是存活时间较长的对象。新生代中的对象使用复制算法,老年代中的对象使用标记整理算法。
服务与服务之间是如何调用的
- RPC远程过程调用,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。在提供RPC服务的微服务中,需要自己指定服务提供者的服务接口和服务方法,并将其注册到RPC框架中,而调用RPC服务的微服务需要调用响应的RPC框架API,并与服务提供方建立网络连接,直接调用目标方法。
- REST调用:这是一种基于HTTP协议方式的万维网软件架构风格,它是一种简单的架构,通常使用JSON数据格式。在提供REST服务的微服务中,需要自己指定服务提供者的服务接口和服务方法,并将其注册到REST框架中,而调用REST服务的微服务需要调用响应的REST框架API,并与服务提供方建立网络连接,直接调用目标方法。
- 消息队列调用:消息队列是一种应用程序对应用程序的通信方法,它通过消息传递在应用程序之间传递数据和信息,将消息放入队列中,消息的接收者从队列中获取消息。在提供消息队列服务的微服务中,需要自己指定服务提供者的服务接口和服务方法,并将其注册到消息队列中,而调用消息队列服务的微服务需要调用响应的消息队列API,并与服务提供方建立网络连接,直接调用目标方法。
- 网关调用:请求首先进入网关,然后由网关代理转发到不同的微服务中,网关可以根据请求的规则和路由信息,根据不同的URL前缀,请求参数,协议等转发到不同的微服务中,网关还可以实现权限控制,流量控制,熔断机制等功能。
数据库连接池如何使用
- 导入数据库连接池相关的依赖库。如:c3p0,druid,dbcp,hikari等
- 配置数据库连接池参数,如:数据库驱动,数据库连接地址,数据库用户名,数据库密码,数据库连接池大小,数据库连接池最大连接数,数据库连接池最小连接数,数据库连接池最大等待时间,数据库连接池最大空闲时间等
- 在应用程序初始化时,创建数据库连接池对象,并设置连接池的参数
- 当需要数据库连接时,从数据库中获取连接
- 使用连接进行数据库操作,如执行SQL语句,执行存储过程等
- 操作完成后释放连接,将连接返回给数据库连接池
- 应用程序关闭时,销毁连接池对象,释放所有连接
// 导入数据库连接池相关依赖
import com.mchange.v2.c3p0.ComboPooledDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
// 配置数据库连接池参数
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClass("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
dataSource.setUser("root");
dataSource.setPassword("root");
dataSource.setInitialPoolSize(5);
dataSource.setMaxPoolSize(10);
dataSource.setMinPoolSize(2);
dataSource.setMaxIdleTime(6000);
// 创建数据库连接池对象
Connection connection = dataSource.getConnection();
// 从连接池中获取连接,进行对应的数据库操作
// 释放连接,将连接返回给数据库连接池
connection.close();
// 销毁连接池对象
dataSource.close();
消息队列
消息队列是一种应用程序对应用程序的通信方法,它通过消息传递在应用程序之间传递数据和信息,将消息放入队列中,消息的接收者从队列中获取消息。消息队列的优点是解耦,异步,削峰,流量控制,缓冲等。
说一些中间件
- Web服务器:Apache,Nginx,IIS
- 数据库中间件:MyBatis,Hibernate,Spring Data JPA
- 消息队列中间件:ActiveMQ,RabbitMQ,Kafka
- 缓存中间件:redis
- 分布式缓存中间件:Ehcache,Memcached,Redis
- 分布式服务框架:Dubbo,Spring Cloud
- 日志中间件:Log4j,Logback,Log4j2
- 安全框架:Spring Security,Apache Shiro
- 监控中间件:Prometheus,Zabbix,Grafana
- 全文搜索引擎:Elasticsearch,Solr
- 定时任务调度中间件:Quartz,xxl-job
- 数据同步中间件:Canal,DataX,Fink-CDC
string为什么是不可变的,底层实现是什么
string是不可变的,是因为string类中的value属性被final修饰,所以value属性的值不能被修改,string类中的其他属性也都是被final修饰的,所以string类是不可变的。string类中的value属性是一个char数组,用于存储字符串的值,当创建一个字符串时,会在堆内存中创建一个string对象,该对象中的value属性指向一个char数组,该数组中存储了字符串的值,当对字符串进行修改时,会重新创建一个string对象,该对象中的value属性指向一个新的char数组,该数组中存储了修改后的字符串的值,原来的char数组中存储的字符串的值不会改变。
不可变的优点:
- 只有当字符串不可变,字符串池才有可能实现
- 数据安全,字符串作为参数传递时,不用考虑是否会被修改
- 实现一个字符串可以被多个线程共享
- 类加载器要用到字符串,不可变性可以保证安全性,以确保类加载器找到的是正确的类
- 因为不可变,使用hashcode进行缓存,不用每次都计算hashcode,这样使得字符串很适合作为Map中的键,字符串的处理速度要快过其他的键对象
- 提高执行效率
唯一索引和主键索引在底层实现上有什么区别
唯一索引和主键索引在底层实现上有以下区别:
唯一索引:唯一索引要求索引列的值必须唯一,可以有空值。在底层实现上,唯一索引使用一种数据结构,例如B+树,来存储索引列的值和对应的行指针。当插入或更新数据时,数据库会检查唯一索引,确保新插入或更新的值不会与已存在的值冲突。
主键索引:主键索引要求索引列的值必须唯一且不能为空。在底层实现上,主键索引与唯一索引类似,也使用一种数据结构来存储索引列的值和对应的行指针。但是,主键索引还会对索引列进行排序,以提高查询性能。
另外,主键索引在数据库中只能有一个,而唯一索引可以有多个。主键索引通常用于标识每一行数据的唯一性,而唯一索引用于确保某一列或多列的值的唯一性。
如何终止一个线程
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
- 使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样,都是作废过期的方法,使用它们可能产生不可预料的结果。
- 使用Thread类提供的interrupt方法中断线程,但是这个方法只是在当前线程中打了一个停止的标记,并不是真正的停止线程,需要用户自己去监视这个标记,使线程退出。
创建一个线程池
使用ThreadPoolExecutor类创建一个线程池,ThreadPoolExecutor类是ExecutorService接口的一个实现类,它可以创建一个线程池,同时还可以提供一些参数来配置线程池的大小,线程池中的线程数量,线程池中的线程的存活时间等。
堆是什么
堆是一种特殊的数据结构,它是一棵完全二叉树,堆中的每一个节点的值都大于或等于其左右孩子节点的值,称为大顶堆,堆中的每一个节点的值都小于或等于其左右孩子节点的值,称为小顶堆。堆中的每一个节点都是一个对象,每一个对象都有一个键值,堆中的每一个节点的键值都大于或等于其左右孩子节点的键值,称为大顶堆,堆中的每一个节点的键值都小于或等于其左右孩子节点的键值,称为小顶堆。
什么是一致性哈希
一致性哈希是一种特殊的哈希算法,它可以将一个对象映射到一个哈希表中的某一个节点上,当哈希表中的节点发生变化时,一致性哈希可以将哈希表中的对象映射到新的节点上,而不会改变哈希表中的其他对象的映射关系。一致性哈希就是一个环,每个节点占一个区间,例如a的区间是a之后b之前(如果b宕机就是a之后c之前),哈希值为这个范围就是a的,好处就是当有节点宕机时候,不至于重新分配哈希范围。
类加载的过程
- 加载:将class文件加载到内存中,并将这些静态数据转换成方法区中的运行时数据结构,在堆中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
- 连接:将Java类的二进制代码合并到JVM的运行状态之中的过程。分为验证,准备,解析三个阶段。验证:确保被加载的类的正确性。准备:为类的静态变量分配内存,并将其初始化为默认值。解析:把类中的符号引用转换为直接引用。
- 初始化:执行类的初始化,包括静态变量赋值,静态代码块执行等。
类加载器:类加载器是用来把类加载到内存中的,JVM中的类加载器有三种:启动类加载器,扩展类加载器,应用程序类加载器。启动类加载器:负责加载JVM自身需要的类,如java.lang.String类。扩展类加载器:负责加载JVM扩展需要的类,如javax.swing包中的类。应用程序类加载器:负责加载应用程序中的类。
JVM中GC原理
GC是垃圾回收的意思,是指在JVM中,当对象没有被引用时,JVM会自动回收该对象,释放内存空间。JVM中的垃圾回收器会定期扫描堆内存中的对象,如果发现某个对象没有被引用,就会将其回收。识别垃圾对象的算法有:引用对象计数法,可达性分析法。
Syncronized的原理
syncronized是一种对象锁,是可重入的,但是不可中断,非公平锁。是基于JVM内置锁实现,通过内部对象Monitor实现,基于进入和退出Monitor对象实现方法与代码块同步,监视器锁的实现底层依赖操作系统的Mutex Lock和Condition来实现的,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么说synchronized效率低的原因。
syncronized锁有四种状态:
- 无锁状态:当没有线程获取到锁时,对象处于无锁状态。
- 偏向锁:同一个线程获取同步资源时,没有别人竞争时,去掉同步操作,相当于没锁
- 轻量级锁:多个线程夺同步资源时,没有获得锁的线程使用CSA自旋等待锁的释放,不会阻塞,提高效率
- 重量级锁:多个线程抢夺同步资源时,使用操作系统的互斥量进行同步,没有获得所的线程阻塞等待唤醒
Thread和Runnable的区别
Runnable是接口,Thread是类且实现Runnable接口,Runnable接口中只有一个run方法,Thread类中有很多方法,Runnable接口可以实现多线程,Thread类只能单继承,Runnable接口可以实现资源共享,Thread类不可以实现资源共享。
快排的实现和时间复杂度
快速排序是一种基于分治的排序算法,它将一个数组分成两个子数组,然后递归地将两个子数组排序,快速排序的实现如下:
void quickSort(int[] arr, int low, int high) {
if (low < high) {
int index = partition(arr, low, high);
quickSort(arr, low, index - 1);
quickSort(arr, index + 1, high);
}
}
int partition(int[] arr, int low, int high) {
int pivot = arr[low];
while (low < high) {
while (low < high && arr[high] >= pivot) {
high--;
}
arr[low] = arr[high];
while (low < high && arr[low] <= pivot) {
low++;
}
arr[high] = arr[low];
}
arr[low] = pivot;
return low;
}
快速排序的时间复杂度为O(nlogn)。当数组中的元素严格有序时,快速排序的时间复杂度为O(n^2)。退化为冒泡排序。
归并排序的实现和时间复杂度
归并排序是一种基于分治的排序算法,它将一个数组分成两个子数组,然后递归地将两个子数组排序,最后将两个子数组合并成一个有序的数组,归并排序的实现如下:
void mergeSort(int[] arr, int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
mergeSort(arr, low, mid);
mergeSort(arr, mid + 1, high);
merge(arr, low, mid, high);
}
}
void merge(int[] arr, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int i = low;
int j = mid + 1;
int k = 0;
while (i <= mid && j <= high) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= high) {
temp[k++] = arr[j++];
}
for (int l = 0; l < temp.length; l++) {
arr[low + l] = temp[l];
}
}
归并排序的时间复杂度为O(nlogn)。
Spring的IOC和AOP
IOC:控制反转,是一种设计思想,将原本在程序中手动创建对象的控制权,交由Spring框架来管理,IOC是一种思想,DI是IOC的一种实现方式,DI:依赖注入,是一种实现IOC的方式,是将创建对象的过程交给Spring框架来管理,对象之间的依赖关系也由Spring框架来管理,对象之间的依赖关系不再写死在代码中,而是通过配置文件来配置,从而提高了代码的可读性和可维护性。
AOP:面向切面编程,是一种编程思想,是对面向对象编程的补充,面向对象编程是将程序分解成一个个对象,而面向切面编程是将程序分解成一个个切面,切面是一种横向的抽取机制,它将程序中的一些公共功能抽取出来,例如日志,事务等,然后将这些公共功能横向抽取到一个切面中,然后将这个切面与程序中的其他功能纵向组合起来,从而提高了代码的可读性和可维护性。
IOC是基于DI实现的,AOP是基于动态代理实现的。
hashmap,hashtable,concurrenthashmap的定义实现和区别
hashmap是一种散列表,它存储的内容是键值对,它允许使用null作为键和值,但是不保证键值对的顺序,它是非线程安全的,它的实现是数组+链表+红黑树,当链表长度大于8时,链表会转换为红黑树,当红黑树节点个数小于6时,红黑树会转换为链表。
线程池如何保证线程按照顺序执行
线程池中的线程是并发执行的,如果需要线程按照顺序执行,可以使用线程池中的submit方法,submit方法可以返回一个Future对象,Future对象可以获取线程的执行结果,可以使用Future对象的get方法,get方法会阻塞当前线程,直到线程执行完成,从而保证线程按照顺序执行。将线程池中的最大线程数设置为1就可以了
synchronized锁如何通知下一个任务
使用wait和notify方法,wait方法会使当前线程等待,notify方法会唤醒一个等待的线程,notifyAll方法会唤醒所有等待的线程。
- notify方法:通知一个在对象上等待的线程,使其从main()方法返回,而返回的前提是该线程获取到了对象的锁;
- notifyAll方法:通知所有等待在该对象上的线程,使它们从wait()方法返回,而返回的前提是它们必须先获取到对象的锁。
- wait方法:使当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法,或者超过指定的时间量。调用wait方法会使当前线程释放对象锁。
四次挥手和三次握手的过程
三次握手:
- 客户端向服务器发送一个SYN报文,报文中会包含一个随机数,用于生成后续的序列号,客户端进入SYN_SEND状态。
- 服务器收到SYN报文后,向客户端发送一个SYN报文和一个ACK报文,ACK报文中的确认号为客户端的随机数加1,服务器进入SYN_RECV状态。
- 客户端收到服务器的SYN报文和ACK报文后,向服务器发送一个ACK报文,ACK报文中的确认号为服务器的随机数加1,客户端进入ESTABLISHED状态,服务器收到ACK报文后,也进入ESTABLISHED状态。
- 三次握手完成,客户端和服务器都进入ESTABLISHED状态,可以开始传输数据了。
第三次握手是可以携带数据的,前两次握手是不可以携带数据的。
四次挥手:
- 客户端向服务器发送一个FIN报文,报文中的序列号为客户端的随机数,客户端进入FIN_WAIT_1状态。
- 服务器收到FIN报文后,向客户端发送一个ACK报文,ACK报文中的确认号为客户端的随机数加1,服务器进入CLOSE_WAIT状态,客户端收到ACK报文后,进入FIN_WAIT_2状态。
- 等待服务端发送完数据后,服务器向客户端发送一个FIN报文,报文中的序列号为服务器的随机数,服务器进入LAST_ACK状态。
- 客户端收到FIN报文后,向服务器发送一个ACK报文,ACK报文中的确认号为服务器的随机数加1,客户端进入TIME_WAIT状态,服务器收到ACK报文后,进入CLOSED状态,客户端等待2MSL后,进入CLOSED状态。
- 四次挥手完成,客户端和服务器都进入CLOSED状态,连接关闭。
DNS的过程和存在的风险
DNS的过程:
- 客户端向本地DNS服务器发送一个DNS请求,请求中包含域名。
- 本地DNS服务器收到DNS请求后,先在本地缓存中查找域名对应的IP地址,如果找到了,就直接返回,如果没有找到,就向根域名服务器发送一个DNS请求。
- 根域名服务器收到DNS请求后,先在本地缓存中查找域名对应的IP地址,如果找到了,就直接返回,如果没有找到,就向顶级域名服务器发送一个DNS请求。
- 顶级域名服务器收到DNS请求后,先在本地缓存中查找域名对应的IP地址,如果找到了,就直接返回,如果没有找到,就向权威域名服务器发送一个DNS请求。
- 权威域名服务器收到DNS请求后,先在本地缓存中查找域名对应的IP地址,如果找到了,就直接返回,如果没有找到,就向本地DNS服务器发送没有找到的响应。
存在的风险:
由于DNS使用的是UDP协议传输消息,消息传输过程中不加密,资源记录也不会进行任何防伪造保护,因此DNS协议容易遭受缓存投毒,数据窃听等攻击。
DNS协议解析过程中,可能会导致用户身份和设备类型等隐私消息泄漏。
由于DNS解析过程的树形结构,一旦根域名服务器或者顶级域名服务器遭受攻击,就会导致整个DNS解析过程无法正常进行。
DNS劫持:DNS劫持是指在DNS解析过程中,将域名解析到错误的IP地址上,从而导致用户访问的是错误的网站,DNS劫持的原理是在DNS解析过程中,将本来应该返回的正确的IP地址替换成了错误的IP地址,DNS劫持的危害是用户访问的是错误的网站,可能会导致用户的隐私信息泄漏,用户的资金损失等。
Java中的反射机制
Java中的反射机制是指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取信息以及动态调用对象方法的功能称为Java的反射机制。
比如spring中的动态代理,就是使用了反射机制,动态代理是指在程序运行时,动态的创建一个代理类,并动态的创建一个代理类的对象,代理类的对象会关联一个InvocationHandler对象,当调用代理类的方法时,会调用InvocationHandler对象的invoke方法,invoke方法中会调用被代理对象的方法,从而实现了动态代理。
注解就是使用了反射机制,注解是一种标记,它可以加在类,方法,属性,参数等上面,注解可以通过反射机制获取,从而实现了动态配置。
但是过渡使用反射机制也会导致安全问题,比如可以无视泛型参数的安全检查。且反射调用方法时,性能会比直接调用方法低很多。
范式清洗
范式清洗是指将数据转换为符合特定规范的格式。在数据处理过程中,经常需要对数据进行清洗,以便更好地进行分析和应用。
范式清洗的具体步骤包括:
去除重复项:检查数据中是否有重复的记录,如果有重复的记录,则需要将其删除或合并。
处理缺失值:检查数据中是否有缺失值,如果有缺失值,则需要进行填充或删除。填充缺失值的方法可以是使用平均值、中位数、众数等统计量进行填充,或者使用插值方法进行填充。
处理异常值:检查数据中是否有异常值,如果有异常值,则需要进行处理。处理异常值的方法可以是删除异常值,或者使用插值方法进行替换。
格式转换:将数据转换为符合特定规范的格式。例如,将日期格式统一为YYYY-MM-DD,将文本格式统一为小写字母等。
数据类型转换:将数据转换为正确的数据类型。例如,将字符串转换为数值型、日期型等。
去除不必要的信息:检查数据中是否有不必要的信息,例如空格、特殊字符等,需要将其去除。
标准化数据:对数据进行标准化处理,使得数据具有相同的尺度和范围。例如,将数值型数据进行归一化处理。
通过范式清洗,可以使得数据更加规范、准确,提高数据的质量和可用性,为后续的数据分析和应用提供基础。
NIO和BIO
BIO:Blcoking IO,同步阻塞型IO,也就是在进行IO操作时,当前线程会被阻塞,直到IO操作完成,当前线程才会继续执行。每一个连接都需要一个独立的线程来处理,如果IO阻塞,线程就会被阻塞,如果线程被阻塞,那么CPU就不能充分利用,会导致系统资源的浪费。
NIO:Non-Blocking IO,同步非阻塞型IO,也就是在进行IO操作时,当前线程不会被阻塞,而是继续执行,当IO操作完成后,会通知当前线程,当前线程再去获取IO操作的结果。一个线程可以处理多个连接,但是IO的读写操作都是同步阻塞的,不能充分利用CPU的性能。NIO引入了几个核心组件:包括Selector,Channel,Buffer,Selector可以注册多个Channel,当Channel发生读写事件时,会通知Selector,Selector会将事件分发给空闲的线程,由线程去处理Channel的读写操作。
序列化和反序列化
序列化是指将对象转换为字节序列的过程,反序列化是指将字节序列转换为对象的过程。序列化和反序列化可以实现对象的持久化,可以将对象保存到文件中,或者通过网络传输对象。
反序列化时,JVM会先从磁盘中读取字节序列,然后将字节序列转换为对象,如果字节序列中的类在JVM中不存在,就会抛出ClassNotFoundException异常。
hashmap
arraylist
gc的四大引用,回收算法等
强引用:强引用是指在程序代码中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾回收器就永远不会回收掉被引用的对象,当内存空间不足时,JVM宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收强引用对象来解决内存不足的问题。
软引用:软引用是一种比较软的引用,可以让对象豁免一些垃圾回收,只有当JVM认为内存不足时,才会去回收软引用对象,软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收掉了,JVM就会把这个软引用加入到与之关联的引用队列中。
弱引用:弱引用是一种比较弱的引用,可以让对象被垃圾回收器回收掉,但是只要垃圾回收器没有回收掉该对象,弱引用就可以使用该对象,弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收器回收掉了,JVM就会把这个弱引用加入到与之关联的引用队列中。
虚引用:虚引用是一种比较弱的引用,如果一个对象仅持有虚引用,那么它和没有任何引用一样,在任何时候都可能被垃圾回收器回收掉,虚引用主要用来跟踪对象被垃圾回收器回收的活动,虚引用必须和引用队列(ReferenceQueue)联合使用,当虚引用所引用的对象被垃圾回收器回收掉后,JVM就会把这个虚引用加入到与之关联的引用队列中。
引用队列:引用队列是用来保存被垃圾回收器回收掉的对象的引用的,当一个对象被垃圾回收器回收掉后,JVM会将该对象的引用加入到与之关联的引用队列中,程序可以通过判断引用队列中是否存在引用来判断被引用的对象是否被垃圾回收器回收掉了。
JVM的运行时数据区域有哪些,作用是
JVM的运行时数据区域包括程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区,运行的常量池。
- 程序计数器:程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
- Java虚拟机栈:Java虚拟机栈是一块线程私有的内存空间,每个线程创建时都会创建一个Java虚拟机栈,Java虚拟机栈中存储的是栈帧,每个方法在执行时都会创建一个栈帧,栈帧中存储了局部变量表,操作数栈,动态链接,方法出口等信息,每个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
- 本地方法栈:本地方法栈和Java虚拟机栈类似,区别是Java虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的Native方法服务。
- Java堆:Java堆是Java虚拟机管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,用于存储对象实例,几乎所有的对象实例都在这里分配内存。
- 方法区:方法区也是被所有线程共享的一块内存区域,在虚拟机启动时创建,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
类加载-双亲委派机制
手写双重较验锁单例模式
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
Android如何判定onPuase()方法后的对象未被回收而导致内存泄漏
MySQL如何避免死锁
- 设置锁等待超时时间:innodb_lock_wait_timeout参数,当一个事务等待锁的时间超过了该参数设置的时间时,就会自动回滚事务,释放锁。
- 按照固定的顺序访问表和行:如果所有的事务都按照固定的顺序访问表和行,就不会出现死锁。
- 对于更新频繁的字段,采用唯一索引的设置方案
- 调整合理的事务隔离级别
- 主动开启死锁检测将参数 innodb_deadlock_detect 设置为 on。当innodb检测发现死锁之后,就会进行回滚死锁的事物。
垃圾回收的过程
什么是分代回收
分代回收是指将堆内存分为新生代和老年代,新生代中的对象一般存活时间较短,老年代中的对象一般存活时间较长,新生代中的对象一般使用复制算法进行垃圾回收,老年代中的对象一般使用标记-清除算法进行垃圾回收。
jdk1.8之后为什么取消了永久代
- 永久代中存放的字符串容易导致性能问题和内存溢出,因为频繁的fullGC会导致应用停顿时间过长
- 永久代的大小不容易确定,原本的-xx:permSize参数用于指定永久代的大小,但是容易出现OOM
- 永久代给垃圾回收机制带来了复杂性,垃圾回收器需要为永久代提供特殊的处理逻辑,而且永久代的回收效率低下
为了解决这个问题,JDK1.8之后,JVM引入了远见作为方法区的实现,元空间并不在虚拟机中,而是使用本地内存,方法区主要存储类的信息,常量池,方法数据,方法代码等信息,元空间的大小受本地内存的限制,元空间的大小可以通过-XX:MaxMetaspaceSize参数设置,当元空间的大小达到了-XX:MaxMetaspaceSize参数设置的大小时,会触发垃圾回收,元空间的垃圾回收主要回收无用的类,常量等信息,元空间的垃圾回收不会导致应用停顿,元空间的垃圾回收效率高。
fullGC和youngGC的区别
FullGC是指对整个堆内存进行垃圾回收,包括新生代和老年代,FullGC的时间一般比较长,会导致应用停顿,FullGC的频率一般比较低,一般是几个小时才会发生一次,FullGC的过程一般会进行老年代的压缩,从而提高老年代的空间利用率,FullGC的过程一般会进行内存整理,从而提高内存的利用率。
触发FullGC的原因:
- 堆内存不足,当堆内存的可用空间不足以分配给新对象时,会触发FullGC,FullGC会对整个堆内存进行垃圾回收,从而释放内存空间,以便给新对象分配内存。
- 调用System.gc()方法,System.gc()方法会通知JVM进行垃圾回收,JVM会进行FullGC,FullGC会对整个堆内存进行垃圾回收。
- 老年代空间不足,当老年代空间不足以分配给新对象时,会触发FullGC,FullGC会对整个堆内存进行垃圾回收,从而释放内存空间,以便给新对象分配内存。
FullGC的过程:
- 标记阶段:标记阶段会对整个堆内存进行垃圾回收,标记阶段会标记出所有存活的对象。
- 可达性分析:检查所有被标记的对象,如果它们所有的引用都被标记了,就会将这些对象进行存活,如果它们所有的引用都没有被标记,就会将这些对象进行回收。
- 清除阶段:清除所有不可达的对象,释放内存空间。
- 压缩阶段:将存活的对象压缩到堆内存的一端,从而提高堆内存的空间利用率。
youngGC是指对新生代进行垃圾回收,youngGC的时间一般比较短,不会导致应用停顿,youngGC的频率一般比较高,一般是几秒钟就会发生一次,youngGC的过程一般不会进行内存整理,因为新生代使用的是复制算法,所以不需要进行内存整理,youngGC的过程一般不会进行老年代的压缩,因为youngGC只会对新生代进行垃圾回收,不会对老年代进行垃圾回收。
触发youngGC的原因:
- 新生代空间不足,当新生代空间不足以分配给新对象时,会触发youngGC,youngGC会对新生代进行垃圾回收,从而释放内存空间,以便给新对象分配内存。
youngGC的过程:
- 标记阶段:标记阶段会对新生代进行垃圾回收,标记阶段会标记出所有存活的对象。
- 复制阶段:将存活的对象复制到新生代的另一块空间中,从而释放出一块空闲的内存空间。
常见的数据类型有哪些
- 基本数据类型:byte,short,int,long,float,double,char,boolean
- 引用数据类型:类,接口,数组
- 基本数据类型的包装类:Byte,Short,Integer,Long,Float,Double,Character,Boolean
float转int的实现
public static int floatToInt(float f) {
return (int) f;
}
String不可变怎么实现的,能不能继承
String不可变是指String对象一旦被创建,就不能被修改,如果需要修改String对象,就会创建一个新的String对象,String不可变的原因是String对象的内部实现是一个char数组,char数组被声明为final,所以String对象一旦被创建,就不能被修改,如果需要修改String对象,就会创建一个新的String对象。
String类是final类,不能被继承。
链表和数组的区别,数组插入该怎么做
数组是一种线性表数据结构,它使用一组连续的内存空间,来存储一组具有相同类型的数据,数组的访问时间复杂度为O(1),数组的插入和删除时间复杂度为O(n)。
链表是一种线性表数据结构,它使用一组任意的内存空间,来存储一组具有相同类型的数据,链表的访问时间复杂度为O(n),链表的插入和删除时间复杂度为O(1)。
数组插入元素时,需要将插入位置后面的元素都向后移动一位,然后将元素插入到插入位置,数组删除元素时,需要将删除位置后面的元素都向前移动一位,然后将最后一个元素置为null。
HashMap结构,转红黑树的时机和原因,key的存储位置怎么计算
HashMap是一种散列表,它存储的内容是键值对,它允许使用null作为键和值,但是不保证键值对的顺序,它是非线程安全的,它的实现是数组+链表+红黑树,当链表长度大于8时,链表会转换为红黑树,当红黑树节点个数小于6时,红黑树会转换为链表。
key的存储位置计算方法:
- 计算key的hashcode
- 计算key的hash值,hash值是hashcode的高16位异或低16位
- 取模预算
线程池的参数,使用线程池的好处
线程池的参数:核心线程数,最大线程数,空闲线程的存活时间,线程工厂,工作队列,拒绝策略
好处:
- 重用存在的线程,减少对象创建、消亡的开销,性能佳。
- 可以有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞。
- 提供定时执行、定期执行、单线程、并发数控制等功能。
Android基本组件
Activity,Service,BroadcastReceiver,ContentProvider
- Activity:通常是用户与应用进行交互的界面,也可以被视为一个单独的屏幕,在这个界面上,可以显示各种控件,并且能够监听并处理用户的事件做出响应
- Service:服务是一种在后台执行操作的组件,并不需要与用户进行交互,但可以在没有用户界面的情况下再后台独立运行。服务可以用于执行长时间运行的操作,例如播放音乐,下载文件等。
- ContentProvider:内容提供者,用于在不同应用程序之间实现数据共享,通过内容提供者,其他应用程序可以访问和操作私有数据
- BroadcastReceiver:广播接收器,用于接收系统或者其他应用程序发送的广播消息,广播接收器可以用于监听系统事件,例如电量变化,网络状态变化等,也可以用于监听其他应用程序的广播消息,例如短信,电话等。
GC算法,为什么会有STW
GC算法:标记-清除算法,复制算法,标记-整理算法,分代收集算法。
标记-清除算法:标记-清除算法分为标记阶段和清除阶段,标记阶段会对堆内存进行垃圾回收,标记阶段会标记出所有存活的对象,清除阶段会清除所有不可达的对象,释放内存空间。
复制算法:复制算法将堆内存分为两块,每次只使用其中一块,当这一块内存使用完了之后,就将存活的对象复制到另一块内存中,然后将这一块内存进行清除,从而释放内存空间。
标记-整理算法:标记-整理算法分为标记阶段和整理阶段,标记阶段会对堆内存进行垃圾回收,标记阶段会标记出所有存活的对象,整理阶段会将存活的对象压缩到堆内存的一端,从而释放出一块连续的内存空间。
分代收集算法:分代收集算法将堆内存分为新生代和老年代,新生代中的对象一般存活时间较短,老年代中的对象一般存活时间较长,新生代中的对象一般使用复制算法进行垃圾回收,老年代中的对象一般使用标记-清除算法进行垃圾回收。
为什么会有STW:STW是指在进行垃圾回收时,会暂停应用程序的执行,STW的原因是因为JVM使用的是分代收集算法,分代收集算法会对整个堆内存进行垃圾回收,当堆内存不足以分配给新对象时,会触发FullGC,FullGC会对整个堆内存进行垃圾回收,从而释放内存空间,以便给新对象分配内存,FullGC会暂停应用程序的执行,从而导致STW。
JVM区域,静态变量存放在哪里
JVM的运行时数据区域包括程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区,运行的常量池。
静态变量存放在方法区中。
算法,反转链表
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
public class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
Java多继承
不支持,多继承会导致菱形继承问题,菱形继承问题是指一个类继承了两个父类,这两个父类又继承了同一个父类,这样就会导致子类中有两个相同的父类,从而导致歧义。
抽象类和接口的区别,什么时候使用抽象类,什么时候使用接口
抽象类和接口的区别:
- 定义和使用上:抽象类是一种不能直接实例化的类,主要用来表示一些具有相似特征的对象的共同特征,如果一个类继承了抽象类,那么就必须实现抽象类中的所有抽象方法,而接口这是一种完全抽象的类型,定义了一组规则,规定了实现它的类必须遵守哪些规范,一个类可以实现多个接口,接口之间可以进行多继承。
- 成员区别:抽象类中可以包含抽象方法和非抽象方法,其中非抽象方法可以直接使用,而接口中只能定义抽象方法,其中的抽象方法必须由实现接口的类来具体实现。
- 构造器上:抽象类可以有构造器,接口不能有构造器。
使用上:如果需要表示一些具有相似特征的对象的共同特征,就可以使用抽象类,如果需要定义一组规则,规定了实现它的类必须遵守哪些规范,就可以使用接口。
线程池的过程,先增加线程还是先加入阻塞队列,为什么
线程池的过程:
- 如果当前线程池中的线程数小于核心线程数,就创建一个新的线程来执行任务。
- 如果正在运行的线程数量大于或等于核心线程数量,那么这个新任务就会被放在阻塞队列中。
- 当一个线程完成任务后,它会返回线程池并尝试获取新的任务来执行
- 如果线程池中的线程数量超过最大线程数,那么就会开始执行拒绝策略。
线程池如何实现核心线程保存在后台的
线程池的核心线程保存在后台可以通过设置线程池的属性来实现。具体步骤如下:
- 创建一个线程池对象,可以使用
ThreadPoolExecutor
类来创建线程池。 - 调用线程池对象的
allowCoreThreadTimeOut(true)
方法,设置核心线程是否保存在后台。 - 设置核心线程的存活时间,可以使用
setKeepAliveTime
方法来设置核心线程的存活时间,单位为毫秒。 - 设置核心线程数,可以使用
setCorePoolSize
方法来设置核心线程数。 - 执行任务,可以使用
execute
方法或submit
方法来提交任务给线程池执行。
通过以上步骤,就可以实现将核心线程保存在后台的线程池了。当没有任务需要执行时,核心线程会根据设置的存活时间自动销毁,从而实现核心线程保存在后台的效果。
哪些对象可以作为GC Roots
- 虚拟机栈中引用的对象,也就是当前执行方法中的引用的对象。
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中JNI引用的对象
- java虚拟机定义的全局引用变量
- 或者的线程
- Stack Local,也就是本地变量表中引用的对象
threadlocal,key为什么是弱引用
ThreadLocalMap是用来存放对象的,在一次线程的执行栈中,存放数据后方便我们在任意的地方取得我们想要的值而不被其他线程干扰。ThreadLocalMap本身并没有为外界提供取出和存放数据的API,我们所能获得数据的方式只有通过ThreadLocal类提供的API来间接的从ThreadLocalMap取出数据,所以,当我们用不了key(ThreadLocal对象)的API也就无法从ThreadLocalMap里取出指定的数据。
在上面的例子中,A对象被回收了,这些get和set方法也访问不到了,也就没法从ThreadLocalMap里取出数据了。没法利用API取出数据,那这个Entry对象还有用吗??所以最好的方法是在A对象被回收后,系统自动回收对应的Entry对象,但是让Entry对象或其中的value对象做为弱引用都是非常不合理的。所以,让key(threadLocal对象)为弱引用,自动被垃圾回收,key就变为null了,下次,我们就可以通过Entry不为null,而key为null来判断该Entry对象该被清理掉了。
至于ThreadLocalMap为什么不给外界提供API来操作数据,我觉得是因为这个Map对于一个线程只有一份,任何地方都在用,为了提供更方便的API和为了我们不破换其他框架保存到里面的数据,所以才用ThreadLocal作为key和API来操作数据。
https的过程
HTTPS是指在HTTP的基础上加入SSL层,HTTPS的过程如下:
- 客户端向服务器端发送一个HTTPS请求,连接到服务器的443端口。
- 服务器端向客户端返回一个证书,证书中包含了服务器的公钥以及有CA对服务器身份的信息
- 客户端会验证证书的合法性,如果证书合法,就会生成一个随机值,然后使用服务器的公钥对随机值进行加密,然后将加密后的随机值发送给服务器端。
- 服务器使用自己的私钥来解密这个对称秘钥,服务器和客户端就可以使用这个对称秘钥来进行加密通信了。
对称加密和非对称加密的区别,常见算法
对称加密是指加密和解密使用的是同一个密钥,对称加密的算法有DES,3DES,AES等。
非对称加密是指加密和解密使用的是不同的密钥,非对称加密的算法有RSA,DSA等。
手撕算法 String转int
public static int stringToInt(String str) {
int result = 0;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
result = result * 10 + (c - '0');
}
return result;
}
tcp和udp的区别
TCP是面向连接的,UDP是面向无连接的。
TCP是一对一的,UDP则是一对多,多对多,一对一都支持。
TCP是可靠的,UDP是不可靠的。
TCP的可靠性是通过超时重传,数据校验,流量控制,拥塞控制等机制来保证的。
TCP是面向字节流的,UDP是面向报文的。
TCP的报文头部比较大,UDP的报文头部比较小。由于TCP头部报文比UDP复杂,使用首部开销比UDP大,
HTTPS的秘钥
HTTPS的秘钥有两种,分别是对称秘钥和非对称秘钥。
对称秘钥是指加密和解密使用的是同一个密钥,对称秘钥的优点是加密和解密速度快,缺点是秘钥的传输不安全,容易被中间人攻击。
非对称秘钥是指加密和解密使用的是不同的密钥,非对称秘钥的优点是秘钥的传输安全,不容易被中间人攻击,缺点是加密和解密速度慢。
HTTPS先使用非对称秘钥加密对称秘钥,然后使用对称秘钥加密数据,这样就可以保证秘钥的传输安全,又可以保证加密和解密的速度。
算法返回链表的倒数第k个节点
public class ListNode{
int val;
ListNode next;
ListNode(int x) { val = x; }
}
public class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head;
ListNode slow = head;
for (int i = 0; i < k; i++) {
fast = fast.next;
}
while (fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
权限修饰符
权限修饰符有public,protected,default,private。
public:公共的,可以被任意访问。
protected:受保护的,可以被同一个包中的类访问,也可以被不同包中的子类访问。
default:默认的,可以被同一个包中的类访问。
private:私有的,只能被同一个类访问。
JVM中的堆和栈,各种数据的存储位置,公有的还是私有的
JVM的运行时数据区域包括程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区,运行的常量池。
程序计数器,Java虚拟机栈,本地方法栈是线程私有的,Java堆,方法区,运行的常量池是线程共享的。
程序计数器:程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。
Java虚拟机栈:Java虚拟机栈是一块线程私有的内存空间,每个线程创建时都会创建一个Java虚拟机栈,Java虚拟机栈中存储的是栈帧,每个方法在执行时都会创建一个栈帧,栈帧中存储了局部变量表,操作数栈,动态链接,方法出口等信息,每个方法从调用到执行完成的过程,对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
本地方法栈:本地方法栈和Java虚拟机栈类似,区别是Java虚拟机栈为虚拟机执行Java方法服务,本地方法栈为虚拟机使用到的Native方法服务。
Java堆:Java堆是Java虚拟机管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建,用于存储对象实例,几乎所有的对象实例都在这里分配内存。
方法区:方法区也是被所有线程共享的一块内存区域,在虚拟机启动时创建,用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。
运行时常量池:运行时常量池是方法区的一部分,用于存储编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。
stringbuilder和stringbuffer,string的区别
String是不可变的,StringBuffer和StringBuilder是可变的,StringBuffer是线程安全的,StringBuilder是线程不安全的。
算法,链表反转
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
public class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
java中的有序集合
TreeSet,TreeMap
垃圾回收的原理
CAS和AQS的区别
CAS是一种乐观锁,AQS是一种悲观锁。
CAS操作包含三个参数,分别是内存地址V,旧的预期值A,即将更新的值B,当且仅当预期值A和内存地址V中的值相同时,才会将内存地址V中的值更新为B,否则不会更新。
AQS是一种基于FIFO队列的锁,AQS中维护了一个FIFO队列,当一个线程获取锁时,如果锁没有被其他线程占用,就会将锁的拥有者设置为当前线程,如果锁被其他线程占用,就会将当前线程加入到FIFO队列中,当锁的拥有者释放锁时,就会从FIFO队列中取出一个线程,将锁的拥有者设置为取出的线程。
CAS一般会遇到什么问题
ABA问题,循环时间长开销大,只能保证一个共享变量的原子操作。
读写锁的实现
读写锁是一种特殊的锁,它允许多个线程同时读共享变量,但是只允许一个线程写共享变量,读写锁的实现可以使用synchronized关键字来实现,也可以使用ReentrantReadWriteLock类来实现。
读写锁可以基于AQS实现吗
可以,读写锁可以基于AQS实现,读写锁的实现可以使用synchronized关键字来实现,也可以使用ReentrantReadWriteLock类来实现,ReentrantReadWriteLock类就是基于AQS实现的。
SQL题,实现含有空值的两张表的连接查询
select * from table1 left join table2 on table1.id = table2.id
可以使用left join来实现含有空值的两张表的连接查询,left join会将左表中的所有数据都查询出来,如果右表中的数据和左表中的数据匹配不上,就会将右表中的数据设置为null。
使用COALESCE函数,实现含有空值的两张表的连接查询
select * from table1 left join table2 on table1.id = table2.id where COALESCE(table1.id, 0) = COALESCE(table2.id, 0)
COALESCE函数可以将null值转换为指定的值,如果COALESCE函数的参数都是null,就会返回null,如果COALESCE函数的参数中有一个不是null,就会返回第一个不是null的参数。
算法 合并区间
public class Solution {
public int[][] merge(int[][] intervals) {
if (intervals.length == 0) {
return new int[0][2];
}
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[0] - o2[0];
}
});
List<int[]> merged = new ArrayList<>();
for (int i = 0; i < intervals.length; i++) {
int left = intervals[i][0];
int right = intervals[i][1];
if (merged.size() == 0 || merged.get(merged.size() - 1)[1] < left) {
merged.add(new int[]{left, right});
} else {
merged.get(merged.size() - 1)[1] = Math.max(merged.get(merged.size() - 1)[1], right);
}
}
return merged.toArray(new int[merged.size()][]);
}
}
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.size() == 0) {
return {};
}
sort(intervals.begin(), intervals.end());
vector<vector<int>> merged;
for (int i = 0; i < intervals.size(); i++) {
int left = intervals[i][0];
int right = intervals[i][1];
if (merged.size() == 0 || merged.back()[1] < left) {
merged.push_back({left, right});
} else {
merged.back()[1] = max(merged.back()[1], right);
}
}
return merged;
}
};
Array.sort()的时间复杂度?底层使用的排序算法
时间复杂度:O(nlogn)
底层使用的排序算法:归并排序,快排,插入排序的混合排序算法,具体使用哪种排序算法取决于数组的大小。
当数组大小小于47时,使用插入排序。
当数组大小大于47且小于286时,使用快排。
当数组大小大于286时,使用归并排序。
volatile底层
volatile是一种轻量级的同步机制,它的底层使用了内存屏障来实现,内存屏障是一种CPU指令,它可以保证指令重排时不会将后面的指令排到内存屏障之前,也不会将前面的指令排到内存屏障之后,内存屏障可以分为LoadLoad屏障,LoadStore屏障,StoreLoad屏障,StoreStore屏障,volatile使用的是StoreLoad屏障,StoreLoad屏障可以保证volatile修饰的变量的写操作不会被重排序到内存屏障之后,读操作不会被重排序到内存屏障之前。
transient底层
transient是Java语言的关键字,它可以修饰成员变量,被transient修饰的成员变量不会被序列化,transient的底层是通过在编译时在成员变量的set方法中加入writeObject方法来实现的,writeObject方法会判断成员变量是否被transient修饰,如果被transient修饰,就不会将成员变量写入到序列化流中。
transient的作用就是把这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化,意思是transient修饰的age字段,他的生命周期仅仅在内存中,不会被写到磁盘中。
ReentrantLock底层
ReentrantLock是一种可重入的互斥锁,它的底层使用了AQS来实现,AQS是一种基于FIFO队列的锁,AQS中维护了一个FIFO队列,当一个线程获取锁时,如果锁没有被其他线程占用,就会将锁的拥有者设置为当前线程,如果锁被其他线程占用,就会将当前线程加入到FIFO队列中,当锁的拥有者释放锁时,就会从FIFO队列中取出一个线程,将锁的拥有者设置为取出的线程。
Object类的方法
Object类是所有类的父类,Object类中的方法有:
- getClass():返回对象的运行时类。
- hashCode():返回对象的哈希码值。
- clone():创建并返回此对象的一个副本。
- toString():返回对象的字符串表示。
- finalize():当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
- equals():指示其他某个对象是否与此对象“相等”。
- wait():导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法。
- notify():唤醒在此对象监视器上等待的单个线程。
- notifyAll():唤醒在此对象监视器上等待的所有线程。
给你一个父类和子类,都有静态变量,常量,构造方法,它们的执行顺序?
在Java中,父类和子类的执行顺序如下:
静态变量和静态代码块:首先,父类的静态变量和静态代码块会按照它们在代码中的顺序依次执行,然后是子类的静态变量和静态代码块。
父类的成员变量和代码块:接下来,父类的成员变量和代码块会按照它们在代码中的顺序依次执行。
父类的构造方法:然后,父类的构造方法会被调用,子类的构造方法中会隐式或显式地调用父类的构造方法。
子类的成员变量和代码块:接着,子类的成员变量和代码块会按照它们在代码中的顺序依次执行。
子类的构造方法:最后,子类的构造方法会被调用。
需要注意的是,静态变量和静态代码块只会在类加载的时候执行一次,而成员变量和代码块会在每次创建对象时执行一次。构造方法在创建对象时被调用,每个对象都会调用一次构造方法。
此外,常量在类加载时会被初始化,并且在每次创建对象时不会重新初始化。因此,常量的初始化顺序与静态变量和静态代码块的执行顺序相同。
JVM内存结构?静态变量在哪?常量在哪?构造函数在哪?
JVM的运行时数据区域包括程序计数器,Java虚拟机栈,本地方法栈,Java堆,方法区,运行的常量池。
程序计数器,Java虚拟机栈,本地方法栈是线程私有的,Java堆,方法区,运行的常量池是线程共享的。
静态变量和常量在方法区中,构造函数在堆中。
Zset的底层实现,为何要使用调表
Zset(有序集合)的底层实现是跳表(Skip List)。
跳表是一种有序链表的扩展结构,它通过在原始链表上增加多级索引来提高查询效率。每一级索引都是原始链表的一个子集,每个节点都包含了下一级索引的指针。这样,在查询时可以通过索引快速定位到目标节点,然后在目标节点所在的链表中进行顺序查找。
跳表相比于普通链表的优势在于它可以提供快速的查找、插入和删除操作,时间复杂度为O(log n),接近于平衡二叉树的效率。而普通链表的查找操作需要遍历整个链表,时间复杂度为O(n)。
为什么要使用跳表作为Zset的底层实现呢?主要有以下几个原因:
有序性:Zset是有序集合,需要能够按照元素的分值(score)进行排序。跳表在构建时可以按照分值进行排序,使得元素在跳表中的顺序是有序的。
快速查找:跳表可以提供快速的查找操作,通过多级索引可以快速定位到目标节点,然后在目标节点所在的链表中进行顺序查找。
空间效率:跳表相比于平衡二叉树等其他数据结构,具有较低的空间复杂度。跳表的每一级索引都是原始链表的一个子集,索引的层数可以根据实际需要进行调整,可以在空间和时间之间进行权衡。
简单性:相比于其他复杂的数据结构,跳表的实现相对简单,容易理解和实现。
总的来说,跳表作为Zset的底层实现,能够提供快速的有序查找操作,同时具有较低的空间复杂度和较简单的实现方式。
Bitmap去重
Bitmap是一种位图,它的每一位只能是0或1,可以用来表示某个元素是否存在,Bitmap的底层是一个数组,数组中的每个元素都是一个字节,每个字节可以表示8个元素,每个元素占用1位,每个元素的值只能是0或1,0表示不存在,1表示存在。
Bitmap去重的原理是,将每个元素映射到Bitmap中的一个位,如果元素存在,就将对应的位设置为1,如果元素不存在,就将对应的位设置为0,这样就可以实现去重了。
数据一致性
数据一致性是指多个副本之间的数据是一致的,数据一致性可以分为强一致性,弱一致性,最终一致性。
强一致性是指多个副本之间的数据是完全一致的,弱一致性是指多个副本之间的数据是有一定延迟的,最终一致性是指多个副本之间的数据是有一定延迟的,但是最终会达到一致的。
线程池是如何工作的
操作系统的核心态和用户态
核心态是指操作系统运行在最高权限级别,可以访问所有的内存空间,可以执行所有的指令,用户态是指操作系统运行在最低权限级别,只能访问受限的内存空间,只能执行受限的指令。
死锁产生的条件
死锁产生的条件有以下四个:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不可剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
破坏死锁的方法
- 破坏互斥条件:将资源改造成可以被多个进程使用。
- 破坏请求与保持条件:一次性申请所有的资源,如果无法满足,就释放已经申请到的资源。等待下次申请时再重新申请。
- 破坏不可剥夺条件:如果一个进程申请到了一些资源,但是在申请其他资源时发生了阻塞,就释放已经申请到的资源。
- 破坏循环等待条件:对所有的资源进行排序,按照顺序申请资源,释放资源时按照逆序释放资源。
hashmap和hashtable的区别
HashMap和Hashtable都是用于存储键值对的集合,它们之间的区别如下:
线程安全性:Hashtable是线程安全的,而HashMap不是。Hashtable的所有方法都是同步的,即在多线程环境下,多个线程可以同时访问Hashtable,不会出现数据不一致的问题。而HashMap在多线程环境下,如果没有适当的同步措施,可能会导致数据不一致的问题。
null键和null值:Hashtable不允许键或值为null,如果尝试将null键或null值放入Hashtable中,会抛出NullPointerException。而HashMap允许键和值都为null,null键会被映射到哈希表的一个桶中,null值可以被映射到任意一个键。
继承关系:Hashtable是Dictionary类的子类,而HashMap是AbstractMap类的子类。Dictionary是一个已过时的类,不推荐使用。
迭代器:Hashtable的迭代器是通过Enumeration实现的,而HashMap的迭代器是通过Iterator实现的。Iterator的功能更强大,支持在迭代过程中删除元素。
性能:由于Hashtable是线程安全的,它的性能通常要比HashMap差一些。在单线程环境下,HashMap的性能更好。
综上所述,如果在多线程环境下需要保证数据的一致性,可以使用Hashtable。如果在单线程环境下或者可以通过其他方式保证数据一致性的情况下,推荐使用HashMap。
short s=1; s=s+1;有什么问题
short s=1; s=s+1;这段代码会报错,因为s+1会自动转换为int类型,int类型不能直接赋值给short类型,需要进行强制类型转换。
泛型的底层实现
泛型是Java语言中的一种语法糖,它的底层是通过类型擦除来实现的,类型擦除是指在编译时期,泛型类型会被擦除为Object类型,泛型方法会被擦除为普通方法,泛型类会被擦除为普通类。
IP协议在哪一层,网络层的作用
IP协议在网络层,网络层的作用是将数据包从源主机传送到目的主机,网络层的协议有IP协议,ICMP协议,ARP协议,RARP协议,IP协议是网络层中最重要的协议,它的作用是将数据包从源主机传送到目的主机。
TCP的断开为何对比连接多一次挥手
TCP的断开需要进行四次挥手,而连接只需要进行三次握手,这是因为TCP是一种面向连接的协议,它要求在数据传输结束后,双方必须彼此确认才能断开连接,以保证数据的完整性和可靠性。
四次挥手的过程如下:
- 发起方发送FIN报文,请求关闭连接。
- 接收方发送ACK报文,确认收到FIN报文。
- 接收方发送FIN报文,请求关闭连接。
- 发起方发送ACK报文,确认收到FIN报文。
这样的过程可以保证数据在传输过程中不会丢失,而且可以避免“半关闭”状态的出现,从而确保连接的可靠性。
synchronized和volatile关键字的区别是什么
synchronized和volatile是Java中用于实现线程同步的关键字,但它们有一些重要的区别。
作用范围不同:synchronized关键字可以用于修饰方法、代码块或者类,而volatile关键字只能用于修饰成员变量。
实现机制不同:synchronized关键字通过获取对象的锁来实现线程同步,只有获得锁的线程才能执行被synchronized修饰的代码块或方法。而volatile关键字通过强制线程从主内存中读取变量的值,而不是从线程的本地缓存中读取,从而保证了变量的可见性。
适用场景不同:synchronized关键字适用于实现线程之间的互斥访问,即同一时间只有一个线程能够执行被synchronized修饰的代码块或方法。volatile关键字适用于实现变量的可见性,即保证一个线程对变量的修改对其他线程可见。
语义不同:synchronized关键字不仅保证了互斥访问,还保证了线程执行完synchronized代码块或方法后会释放锁。而volatile关键字只保证了变量的可见性,不保证原子性和互斥访问。
总结来说,synchronized关键字适用于实现复杂的线程同步,而volatile关键字适用于实现简单的变量可见性。