说在前面
在40岁老架构师尼恩的(50+)读者社区中,经常有小伙伴,需要面试美团、京东、阿里、 百度、头条等大厂。
下面是一个小伙伴成功拿到通过了美团一次技术面试,最终,小伙伴通过后几面技术拷问、灵魂拷问,最终拿到offer。
从这些题目来看:美团的面试,偏重底层知识和原理,大家来看看吧。
现在把面试真题和参考答案收入咱们的宝典,大家看看,收个美团Offer需要学点啥?
当然对于中高级开发来说,这些面试题,也有参考意义。
这里把题目以及参考答案,收入咱们的《尼恩Java面试宝典》 V84版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到公号【技术自由圈】取
文章目录
- 说在前面
- 美团一面索命44问
- 1、说一说Java内存区域和内存模型
- 一、内存区域
- 本地方法栈
- 程序计数器
- Java虚拟机栈
- 堆
- 元空间
- 二、内存模型
- 计算机高速缓存和缓存一致性
- JVM 主内存与工作内存
- 重排序
- happens-before
- Java内存模型的实现
- 2、什么是分布式系统?
- 3、分布式系统你会考虑哪些方面?
- 4、为什么说TCP/IP协议是不可靠的?
- 5、OSI有哪七层模型?TCP/IP是哪四层模型?
- 一、什么是OSI?
- 二、什么是TCP/IP四层模型?
- 三、OSI七层网络模型和TCP/IP四层网络模型的关系:
- 四、OSI七层和TCP/IP的区别
- 6、讲一讲TCP协议的三次握手和四次挥手流程
- 一、三次握手
- 二、四次挥手
- 三、种状态名词解析
- 7、为什么TCP建立连接协议是三次握手,而关闭连接却是四次握手呢?为什么不能用两次握手进行连接?
- 一、为什么建立连接需要三次握手?
- 二、为什么关闭连接需要四次握手?
- 8、http 请求头里,expire 和 cache-control 字段含义,说说 HTTP 状态码
- 9、说说 Redis 为什么快
- 10、Redis 有几种持久化方式
- 一、RDB持久化:
- 二、AOF持久化:
- 11、Redis 挂了怎么办?
- 12、多线程情况下,如何保证线程安全?
- 一、线程安全等级
- 1. 不可变
- 2. 绝对线程安全
- 3. 相对线程安全
- 4. 线程兼容
- 5. 线程对立
- 二、线程安全的实现方法
- 1. 互斥同步
- 2. 非阻塞同步
- 3. 无需同步方案
- 13、用过volatile 吗?它是如何保证可见性的,原理是什么呢?
- 14、谈谈你对volatile 关键字作用和原理的理解
- 一、可见性
- 二、禁止重排序
- 总结
- 15、聊聊分库分表,分表为什么要停服这种操作,如果不停服可以怎么做?
- 一、分库分表
- 二、分表为什么要停服
- 三、如果不停服可以怎么做
- 16、Java虚拟机中,数据类型可以分为哪几类?
- 17、怎么理解栈、堆?堆中存什么?栈中存什么?
- 一、栈(Stack):
- 二、堆(Heap):
- 总结:
- 18、为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
- 19、为什么不把基本类型放堆中呢?
- 20、Java中的参数传递时传值呢?还是传引用?
- 21、Java中有没有指针的概念?
- 22、Java中,栈的大小通过什么参数来设置?
- 23、一个空Object对象的占多大空间?
- 24、对象引用类型分为哪几类?
- 25、讲一讲垃圾回收算法
- 26、如何解决内存碎片的问题?
- 27、说说JVM底层原理和排查命令
- 28、说说ZK一致性原理
- 29、说说Redis数据结构、持久化、哨兵、cluster数据分片规则
- 一、数据结构:
- 二、持久化:
- 三、哨兵(Sentinel):
- 四、集群(Cluster):
- 30、Kafka一致性原理,消费时的消息丢失和重复如何解决?
- 31、说说微服务优缺点
- 优点:
- 缺点:
- 32、说说synchronizedlock的底层实现
- 一、synchronized的底层实现:
- 二、Lock的底层实现:
- 33、说说hashmap的底层实现
- 一、数据结构:
- 二、哈希算法:
- 三、解决哈希冲突:
- 四、扩容:
- 34、说说Java的序列化底层实现
- 35、说说MySQL的底层实现
- 36、说说Spring IOC, AOP, MVC的底层实现大致逻辑
- 一、Spring IOC的底层实现逻辑:
- 二、Spring AOP的底层实现逻辑:
- 三、Spring MVC的底层实现逻辑:
- 37、大致说下你熟悉的框架中用到的设计模式
- 一、Spring框架:
- 二、Apache Kafka:
- 三、MyBatis框架:
- 四、Spring Boot框架:
- 38、说说项目中用到的设计模式
- 39、说说Netty的主要组件
- 40、使用dubbo进行远程调用时消费端需要几个线程
- 41、说说内存分配以及优化
- 一、内存分配方式:
- 二、内存分配优化:
- 三、内存分配的注意事项:
- 42、你怎么防止优惠券有人重复刷?
- 43、有一个整型数组,数组元素不重复,数组元素先升序后
- 44、降序,找出最大值
- 尼恩说在最后
- 推荐相关阅读
美团一面索命44问
1、说一说Java内存区域和内存模型
Java 内存区域和内存模型是不一样的东西。
内存区域:JVM 运行时将数据分区域存储,强调对内存空间的划分。
内存模型(Java Memory Model,简称 JMM ):定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式。
一、内存区域
下图是 JDK 1.8 之前的 JVM 运行时数据区域分布图:
下图是 JDK 1.8 之后的 JVM 运行时数据区域分布图:
通过 JDK 1.8 之前与 JDK 1.8 之后的 JVM 运行时数据区域分布图对比,我们可以发现区别就是 1.8有一个元空间替代方法区。下文元空间章节
介绍了为何替换方法区。
下面我们针对 JDK 1.8 之后的 JVM 内存分布图介绍每个区域的它们是干什么的。
本地方法栈
Native Method Stacks:是为虚拟机使用到的 Native 方法服务,可以认为是通过 JNI
(Java Native Interface) 直接调用本地 C/C++ 库,不受 JVM 控制。
我们常用
获取当前时间毫秒
就是 Native 本地方法,方法被native
关键字修饰。
package java.lang;
public final class System {
public static native long currentTimeMillis();
}
其实就是为了解决一些 Java 本身做不到,但是 C/C++ 可以,通过 JNI 扩展 Java 的使用,融合不同的编程语言。
程序计数器
Program Counter Register:一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。由于 JVM 可以并发执行线程,所以会为每个线程分配一个程序计数器,与线程的生命周期相同。因此会存在线程之间的切换,而这个时候就程序计数器会记录下当前程序执行到的位置,以便在其他线程执行完毕后,恢复现场继续执行。
如果线程正在执行的是 Java 方法,这个计数器记录的是正在执行虚拟机字节码指令的地址;如果正在执行的是 Native 方法,计数器的值则为空(undefined)
此内存区域是唯一一个
在 Java 虚拟机规范中没有规定任何 OutOfMemoryError
情况的区域。
Java虚拟机栈
Java Virtual Machine Stacks:与程序计数器一样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型
:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表
、操作栈
、动态链接
、方法出口
等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
栈是一个先入后出(FILO-First In Last Out)的有序列表。在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。
局部变量表
局部变量表是存放方法参数和局部变量的区域。局部变量没有准备阶段,必须显式初始化。全局变量是放在堆的,有两次赋值的阶段,一次在类加载的准备阶段,赋予系统初始值;另外一次在类加载的初始化阶段,赋予代码定义的初始值。
操作栈
操作栈是个初始状态为空的桶式结构栈(先入后出)。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。
动态链接
每个栈帧都包含一个指向运行时常量池中对当前方法的引用,目的是支持方法调用过程的动态连接。当前方法中如果需要调用其他方法的时候,能够从运行时常量池中找到对应的符号引用,然后将符号引用转换为直接引用,然后就能直接调用对应方法。
不是所有方法调用都需要动态链接的,有一部分符号引用会在类加载解析阶段
将符号引用转换为直接引用,这部分操作称之为: 静态解析
,就是编译期间就能确定调用的版本,包括: 调用静态方法, 调用实例的私有构造器, 私有方法,父类方法。
方法返回地址
方法执行时有两种退出情况:
- 正常退出,即正常执行到任何方法的返回字节码指令,如 RETURN、IRETURN、ARETURN 等;
- 异常退出。
无论何种退出情况,都将返回至方法当前被调用的位置。方法退出的过程相当于弹出当前栈帧。
堆
我们经常说的 GC 调优/JVM 调优,99%指的都是
调堆
!Java 栈、本地方法栈、程序计数器这些一般不会产生垃圾。
Heap:Java 虚拟机所管理的内存中最大的一块。Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”(Garbage Collected Heap)。
元空间
JDK 1.8就把方法区改用元空间了。类的元信息被存储在元空间中,元空间没有使用堆内存,而是与堆不相连的本地内存区域。所以,理论上系统可以使用的内存有多大,元空间就有多大。
方法区
Method Area:与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。
方法区与元空间的变迁
下图是 JDK 1.6、JDK 1.7 到 JDK 1.8 方法区的大致变迁过程:
JDK 1.8 中 HotSpot JVM 移出 永久代(PermGen),开始时使用元空间(Metaspace)。使用元空间取代永久代的实现的主要原因如下:
- 避免OOM异常,字符串存在永久代中,容易出现性能问题和内存溢出;
- 永久代设置空间大小是很难确定,太小容易出现永久代溢出,太大则容易导致老年代溢出;
- 永久代进行调优非常困难;
- 将 HotSpot 与 JRockit 合二为一;
二、内存模型
内存模型是为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。
内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障。
Java 内存模型(JMM)控制 Java 线程之间的通信,决定一个线程对共享变量的写入何时对另一个线程可见。
计算机高速缓存和缓存一致性
计算机在高速的 CPU 和相对低速的存储设备之间使用高速缓存,作为内存和处理器之间的缓冲。当程序在运行过程中,会将运算需要的数据从主存(计算机的物理内存)复制一份到 CPU 的高速缓存当中,那么 CPU 进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。
在多处理器的系统中(或者单处理器多核的系统),每个处理器内核都有自己的高速缓存,它们有共享同一主内存(Main Memory)。当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到,再从二级缓存中查找,如果还是没有就从三级缓存(不是所有 CPU 都有三级缓存
)或内存中查找。
在多核 CPU 中,每个核在自己的缓存中,关于同一个数据的缓存内容可能不一致。为此,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,来维护缓存的一致性。
JVM 主内存与工作内存
Java 内存模型中规定了所有的变量都存储在主内存
中,每条线程还有自己的工作内存
,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
这里的工作内存是 JMM 的一个抽象概念,其存储了该线程以读 / 写共享变量的副本
。
重排序
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:
- 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序:由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
Java 编译器禁止处理器重排序是通过在生成指令序列的适当位置会插入内存屏障
(重排序时不能把后面的指令重排序到内存屏障之前的位置)指令来实现的。
happens-before
Java 内存模型提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
Java内存模型的实现
在Java中提供了一系列和并发处理相关的关键字,比如 volatile
、synchronized
、final
、JUC 包
等。其实这些就是 Java 内存模型封装了底层的实现后提供给程序员使用的一些关键字。
原子性
为了保证原子性,提供了两个高级的字节码指令 monitorenter
和 monitorexit
,这两个字节码,在Java中对应的关键字就是 synchronized
。
我们对
synchronized
关键字都很熟悉,你们可以把下面的代码编译成 class 文件,用javap -v SyncViewByteCode.class
查看字节码,就可以找到monitorenter
和monitorexit
字节码指令。
public class SyncViewByteCode {
public synchronized void buy() {
System.out.println("buy porsche");
}
}
字节码,部分结果如下:
public com.dolphin.thread.locks.SyncViewByteCode();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/dolphin/thread/locks/SyncViewByteCode;
public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
Exception table:
可见性
Java 内存模型是通过在变量修改后
将新值同步回主内存,在变量读取前
从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。
Java 中的 volatile
关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用 volatile
来保证多线程操作时变量的可见性。
除了 volatile
,Java中的 synchronized
和 final
两个关键字也可以实现可见性。只不过实现方式不同。
有序性
在 Java 中,可以使用 synchronized
和 volatile
来保证多线程之间操作的有序性。实现方式有所区别:
volatile
:关键字会禁止指令重排。synchronized
:关键字保证同一时刻只允许一条线程操作。
2、什么是分布式系统?
分布式系统是由多台计算机组成的网络系统,这些计算机通过消息传递或共享存储等方式协同工作,以实现共同的目标。分布式系统的设计目标是将计算和数据分布在多个节点上,以提供更高的性能、可扩展性和可靠性。
在分布式系统中,各个节点可以独立地执行任务,并通过通信协议进行相互通信和协调。这些节点可以是物理上分布在不同地理位置的计算机,也可以是虚拟机、容器或云服务实例。
分布式系统的特点包括:
- 并行处理:分布式系统可以同时处理多个任务,通过将工作分配给不同的节点并行执行,提高系统的处理能力。
- 可扩展性:分布式系统可以根据需求增加或减少节点,以适应不同规模和负载的变化。通过添加更多的节点,系统可以处理更多的请求并提供更高的性能。
- 容错性:分布式系统可以通过冗余和备份机制来提高系统的可靠性和容错性。当某个节点发生故障时,系统可以继续运行,并且可以通过其他节点接管故障节点的工作。
- 数据共享和协调:分布式系统通过共享存储或消息传递等方式实现节点之间的数据共享和协调。这使得不同节点之间可以共享数据、状态和资源,并协同完成复杂的任务。
分布式系统的设计和管理需要考虑到网络通信、一致性、并发控制、故障处理等方面的挑战。同时,分布式系统也提供了更高的灵活性和可靠性,被广泛应用于各种领域,如云计算、大数据处理、物联网等。
3、分布式系统你会考虑哪些方面?
在设计和管理分布式系统时,需要考虑以下方面:
- 可靠性和容错性:分布式系统应具备容错能力,能够在节点故障或网络中断等情况下继续正常运行。这可以通过冗余备份、故障检测和自动恢复机制来实现。
- 可扩展性:分布式系统应具备良好的可扩展性,能够根据需求增加或减少节点,以适应不同规模和负载的变化。这可以通过水平扩展和垂直扩展等方式来实现。
- 数据一致性:在分布式系统中,由于数据分布在多个节点上,需要确保数据的一致性。这可以通过一致性协议(如Paxos、Raft)和副本同步机制来实现。
- 通信和协议:分布式系统中的节点需要进行通信和协调工作,因此需要选择适合的通信协议和消息传递机制。常见的通信协议包括TCP/IP、HTTP、RPC等。
- 负载均衡:为了提高系统的性能和可用性,需要在节点之间均衡地分配负载。负载均衡可以通过请求调度算法和动态负载均衡策略来实现。
- 安全性:分布式系统需要保护数据的机密性、完整性和可用性,因此需要考虑安全性措施,如身份验证、数据加密、访问控制等。
- 监控和诊断:分布式系统应具备监控和诊断功能,能够实时监测系统的运行状态和性能指标,并及时发现和解决问题。
- 部署和管理:分布式系统的部署和管理需要考虑节点的配置、软件的安装和更新、版本控制等方面的问题。自动化部署和管理工具可以提高效率和可靠性。
- 数据备份和恢复:为了应对节点故障或数据丢失的情况,需要进行数据备份和恢复。常见的备份策略包括冷备份、热备份和增量备份等。
- 性能优化:分布式系统的性能优化涉及到各个方面,包括算法优化、数据结构设计、并发控制、缓存策略等。
综上所述,设计和管理分布式系统需要综合考虑可靠性、可扩展性、数据一致性、通信和协议、负载均衡、安全性、监控和诊断、部署和管理、数据备份和恢复以及性能优化等方面的问题。
4、为什么说TCP/IP协议是不可靠的?
TCP/IP协议被认为是可靠的协议,因为它提供了许多机制来确保数据的可靠传输。
然而,有时人们会说TCP/IP协议是不可靠的,这是因为在特定情况下,它可能无法满足用户的要求或出现一些问题。
以下是一些可能导致TCP/IP协议被称为不可靠的情况:
- 丢包:在网络传输过程中,由于网络拥塞、设备故障或其他原因,数据包可能会丢失。尽管TCP协议具有重传机制,但在某些情况下,丢失的数据包可能无法被及时重传,从而导致数据传输的不可靠性。
- 延迟:TCP/IP协议使用拥塞控制机制来确保网络的稳定性和公平性。这意味着在网络拥塞时,数据传输可能会出现延迟。对于某些对实时性要求较高的应用,如在线游戏或视频通话,这种延迟可能被认为是不可靠的。
- 顺序问题:TCP协议保证数据包的按序传输,但在某些情况下,数据包的顺序可能会被打乱。例如,当数据包在网络中的不同路径上传输时,它们可能以不同的顺序到达目的地。这可能导致数据包的重组和排序问题,从而影响数据的可靠性。
需要注意的是,尽管TCP/IP协议在某些情况下可能会出现问题,但它仍然是互联网上最常用的协议之一,被广泛应用于各种应用和服务中。此外,TCP/IP协议还可以通过配置和优化来提高其可靠性和性能。
5、OSI有哪七层模型?TCP/IP是哪四层模型?
一、什么是OSI?
OSI 模型(Open System Interconnection model)是一个由国际标准化组织提出的概念模型,试图提供一个使各种不同的计算机和网络在世界范围内实现互联的标准框架。
它将计算机网络体系结构划分为七层,每层都可以??供抽象良好的接口。
从上到下可分为七层:每一层都完成特定的功能,并为上一层提供服务,并使用下层所提供的服务。
- 物理层:
物理层负责最后将信息编码成电流脉冲或其它信号用于网上传输;
eg:RJ45等将数据转化成0和1; - 数据链路层:
数据链路层通过物理网络链路提供数据传输。不同的数据链路层定义了不同的网络和协议特征,其中包括物理编址、网络拓扑结构、错误校验、数据帧序列以及流控;
可以简单的理解为:规定了0和1的分包形式,确定了网络数据包的形式; - 网络层:
网络层负责在源和终点之间建立连接;
可以理解为,此处需要确定计算机的位置,怎么确定?IPv4,IPv6! - 传输层:
传输层向高层提供可靠的端到端的网络数据流服务。
可以理解为:每一个应用程序都会在网卡注册一个端口号,该层就是端口与端口的通信!常用的(TCP/IP)协议; - 会话层:
会话层建立、管理和终止表示层与实体之间的通信会话;
建立一个连接(自动的手机信息、自动的网络寻址); - 表示层:
表示层提供多种功能用于应用层数据编码和转化,以确保以一个系统应用层发送的信息 可以被另一个系统应用层识别;
可以理解为:解决不同系统之间的通信,eg:Linux下的QQ和Windows下的QQ可以通信; - 应用层:
OSI 的应用层协议包括文件的传输、访问及管理协议(FTAM) ,以及文件虚拟终端协议(VIP)和公用管理系统信息(CMIP)等;
规定数据的传输协议;
二、什么是TCP/IP四层模型?
应用层(Application):为用户提供所需要的各种服务
传输层(Transport):为应用层实体提供端到端的通信功能,保证了数据包的顺序传送及数据的完整性
网际层(Internet):主要解决主机到主机的通信问题
网络接口层(Network Access):负责监视数据在主机和网络之间的交换
三、OSI七层网络模型和TCP/IP四层网络模型的关系:
OSI引入了服务、接口、协议、分层的概念,TCP/IP借鉴了OSI的这些概念建立TCP/IP模型。
OSI先有模型,后有协议,先有标准,后进行实践;而TCP/IP则相反,先有协议和应用再提出了模型,且是参照的OSI模型。
OSI是一种理论下的模型,而TCP/IP已被广泛使用,成为网络互联事实上的标准。
OSI七层网络模型 | TCP/IP四层概念模型 | 对应网络协议 |
---|---|---|
应用层(Application) | 应用层 | HTTP、TFTP, FTP, NFS, WAIS、SMTP |
表示层(Presentation) | Telnet, Rlogin, SNMP, Gopher | |
会话层(Session) | SMTP, DNS | |
传输层(Transport) | 传输层 | TCP, UDP |
网络层(Network) | 网络层 | IP, ICMP, ARP, RARP, AKP, UUCP |
数据链路层(Data Link) | 数据链路层 | FDDI, Ethernet, Arpanet, PDN, SLIP, PPP |
物理层(Physical) | IEEE 802.1A, IEEE 802.2到IEEE 802.11 |
四、OSI七层和TCP/IP的区别
- TCP/IP他是一个协议簇;而OSI(开放系统互联)则是一个模型,且TCP/IP的开发时间在OSI之前。
- TCP/IP是由一些交互性的模块做成的分层次的协议,其中每个模块提供特定的功能;OSI则指定了哪个功能是属于哪一层的。
- TCP/IP是四层结构,而OSI是七层结构。OSI的最高三层在TCP中用应用层表示。
40岁老架构师尼恩提示:
TCP/IP既是面试的绝对重点,也是面试的绝对难点,建议大家有一个深入和详细的掌握,具体的内容请参见《尼恩Java面试宝典-专题10:TCP/IP协议》PDF,该专题对TCP/IP有一个系统化、体系化、全面化的介绍。
6、讲一讲TCP协议的三次握手和四次挥手流程
TCP的三次握手和四次挥手实质就是TCP通信的连接和断开。
三次握手:为了对每次发送的数据量进行跟踪与协商,确保数据段的发送和接收同步,根据所接收到的数据量而确认数据发送、接收完毕后何时撤消联系,并建立虚连接。
四次挥手:即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。
TCP三次握手、四次挥手时序图
一、三次握手
TCP协议位于传输层,作用是提供可靠的字节流服务,为了准确无误地将数据送达目的地,TCP协议采纳三次握手策略。
三次握手原理:
第1次握手:客户端发送一个带有SYN(synchronize)标志的数据包给服务端;
第2次握手:服务端接收成功后,回传一个带有SYN/ACK标志的数据包传递确认信息,表示我收到了;
第3次握手:客户端再回传一个带有ACK标志的数据包,表示我知道了,握手结束。
其中:SYN标志位数置1,表示建立TCP连接;ACK标志表示验证字段。
可通过以下趣味图解理解三次握手:
三次握手过程详细说明:
- 客户端发送建立TCP连接的请求报文,其中报文中包含seq序列号,是由发送端随机生成的,并且将报文中的SYN字段置为1,表示需要建立TCP连接。(SYN=1,seq=x,x为随机生成数值);
- 服务端回复客户端发送的TCP连接请求报文,其中包含seq序列号,是由回复端随机生成的,并且将SYN置为1,而且会产生ACK字段,ACK字段数值是在客户端发送过来的序列号seq的基础上加1进行回复,以便客户端收到信息时,知晓自己的TCP建立请求已得到验证。(SYN=1,ACK=x+1,seq=y,y为随机生成数值)这里的ack加1可以理解为是确认和谁建立连接;
- 客户端收到服务端发送的TCP建立验证请求后,会使自己的序列号加1表示,并且再次回复ACK验证请求,在服务端发过来的seq上加1进行回复。(SYN=1,ACK=y+1,seq=x+1)。
二、四次挥手
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
四次挥手原理:
第1次挥手:客户端发送一个FIN,用来关闭客户端到服务端的数据传送,客户端进入FIN_WAIT_1状态;
第2次挥手:服务端收到FIN后,发送一个ACK给客户端,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),服务端进入CLOSE_WAIT状态;
第3次挥手:服务端发送一个FIN,用来关闭服务端到客户端的数据传送,服务端进入LAST_ACK状态;
第4次挥手:客户端收到FIN后,客户端t进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,服务端进入CLOSED状态,完成四次挥手。
其中:FIN标志位数置1,表示断开TCP连接。
可通过以下趣味图解理解四次挥手:
四次挥手过程详细说明:
- 客户端发送断开TCP连接请求的报文,其中报文中包含seq序列号,是由发送端随机生成的,并且还将报文中的FIN字段置为1,表示需要断开TCP连接。(FIN=1,seq=x,x由客户端随机生成);
- 服务端会回复客户端发送的TCP断开请求报文,其包含seq序列号,是由回复端随机生成的,而且会产生ACK字段,ACK字段数值是在客户端发过来的seq序列号基础上加1进行回复,以便客户端收到信息时,知晓自己的TCP断开请求已经得到验证。(FIN=1,ACK=x+1,seq=y,y由服务端随机生成);
- 服务端在回复完客户端的TCP断开请求后,不会马上进行TCP连接的断开,服务端会先确保断开前,所有传输到A的数据是否已经传输完毕,一旦确认传输数据完毕,就会将回复报文的FIN字段置1,并且产生随机seq序列号。(FIN=1,ACK=x+1,seq=z,z由服务端随机生成);
- 客户端收到服务端的TCP断开请求后,会回复服务端的断开请求,包含随机生成的seq字段和ACK字段,ACK字段会在服务端的TCP断开请求的seq基础上加1,从而完成服务端请求的验证回复。(FIN=1,ACK=z+1,seq=h,h为客户端随机生成)
至此TCP断开的4次挥手过程完毕。
三、种状态名词解析
LISTEN:等待从任何远端TCP 和端口的连接请求。
SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。
SYN_RECEIVED:发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。
ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。
FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。
FIN_WAIT_2:等待远端TCP 的连接终止请求。
CLOSE_WAIT:等待本地用户的连接终止请求。
CLOSING:等待远端TCP 的连接终止请求确认。
LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)
TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。
TIME_WAIT 两个存在的理由:
1.可靠的实现tcp全双工连接的终止;
2.允许老的重复分节在网络中消逝。
CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)
7、为什么TCP建立连接协议是三次握手,而关闭连接却是四次握手呢?为什么不能用两次握手进行连接?
TCP使用三次握手来建立连接,而使用四次握手来关闭连接,主要是为了确保通信双方的状态同步和可靠性。下面是详细解释:
一、为什么建立连接需要三次握手?
- 第一次握手:客户端发送一个带有SYN(同步)标志的数据包到服务器,请求建立连接,并进入SYN_SENT状态。
- 第二次握手:服务器接收到客户端的请求后,回复一个带有SYN/ACK(同步/确认)标志的数据包,表示同意建立连接,并进入SYN_RCVD状态。
- 第三次握手:客户端收到服务器的回复后,再发送一个带有ACK(确认)标志的数据包,表示连接建立成功,双方可以开始通信,客户端和服务器都进入ESTABLISHED状态。
三次握手的目的是确保双方都能够收到对方的确认消息,以建立可靠的连接。如果只有两次握手,那么可能会出现以下情况:
- 客户端发送了连接请求,但由于网络延迟等原因,该请求在传输过程中被丢失,服务器无法知道客户端的请求。
- 服务器接收到客户端的连接请求后,发送了确认,但由于网络延迟等原因,该确认在传输过程中被丢失,客户端无法知道服务器的确认。
如果只有两次握手,上述情况下客户端和服务器都无法确认对方是否接收到了自己的请求或确认,从而无法建立可靠的连接。因此,通过三次握手可以确保双方都能够确认连接的建立。
二、为什么关闭连接需要四次握手?
- 第一次握手:当一方决定关闭连接时,发送一个带有FIN(结束)标志的数据包,表示不再发送数据,但仍然可以接收数据,进入FIN_WAIT_1状态。
- 第二次握手:另一方收到FIN后,发送一个带有ACK标志的数据包作为确认,表示收到了关闭请求,进入CLOSE_WAIT状态。
- 第三次握手:另一方发送一个带有FIN标志的数据包,表示同意关闭连接,进入LAST_ACK状态。
- 第四次握手:请求关闭连接的一方收到确认后,发送一个带有ACK标志的数据包,表示连接关闭,进入TIME_WAIT状态。
四次握手的目的是确保双方都能够完成数据的传输和确认,以避免数据丢失或混乱。关闭连接时,双方需要交换确认信息,以确保对方知道连接已关闭,并且不会再发送数据。
如果只有三次握手,可能会出现以下情况:
- 一方发送了关闭请求,但对方没有收到,导致连接一直处于半关闭状态。
- 一方发送了关闭请求后,对方直接关闭连接,而不等待数据传输完成,导致数据丢失。
通过四次握手,可以确保双方都能够完成数据传输和确认,从而安全地关闭连接。
需要注意的是,四次握手中的最后一次握手(第四次)是为了确保连接的可靠关闭,并且在关闭后一段时间内等待可能延迟的数据包到达,以防止出现连接复用时的问题。
40岁老架构师尼恩提示:TCP/IP既是面试的绝对重点,也是面试的绝对难点,
建议大家有一个深入和详细的掌握,具体的内容请参见《尼恩Java面试宝典-专题10:TCP/IP协议》PDF,该专题对TCP/IP有一个系统化、体系化、全面化的介绍。
8、http 请求头里,expire 和 cache-control 字段含义,说说 HTTP 状态码
在HTTP请求头中,Expires
和Cache-Control
字段用于控制缓存的行为。
Expires
字段指定了一个绝对的过期时间,表示在该时间之后,缓存的副本将被认为是过期的。服务器在返回响应时,会在响应头中包含Expires
字段,告知客户端缓存的有效期。客户端在接收到响应后,会将该响应缓存起来,并在过期时间之前使用缓存的副本。然而,Expires
字段存在一些问题,比如服务器和客户端的时钟不同步可能导致缓存失效。
为了解决Expires
字段的问题,引入了Cache-Control
字段。Cache-Control
字段提供了更加灵活和可靠的缓存控制机制。它可以包含多个指令,用逗号分隔,每个指令都有特定的含义和参数。常见的指令包括:
max-age=<seconds>
:指定缓存的最大有效期,以秒为单位。no-cache
:表示缓存副本需要重新验证,不能直接使用。no-store
:表示不缓存任何副本,每次请求都需要重新获取资源。public
:表示响应可以被任意缓存存储。private
:表示响应只能被单个用户缓存,通常用于私有数据。
HTTP状态码用于表示服务器对请求的响应状态。常见的HTTP状态码包括:
- 1xx:信息性状态码,表示请求已被接收,继续处理。
- 2xx:成功状态码,表示请求已成功被接收、理解、并处理。
- 3xx:重定向状态码,表示需要进一步操作以完成请求。
- 4xx:客户端错误状态码,表示请求包含错误或无法完成请求。
- 5xx:服务器错误状态码,表示服务器在处理请求时发生了错误。
一些常见的状态码包括:
- 200 OK:请求成功,服务器成功处理了请求。
- 301 Moved Permanently:永久重定向,请求的资源已永久移动到新位置。
- 400 Bad Request:客户端请求有语法错误,服务器无法理解。
- 404 Not Found:请求的资源不存在。
- 500 Internal Server Error:服务器内部错误,无法完成请求。
状态码提供了一种标准化的方式,使得客户端和服务器能够准确地了解请求的处理结果,并采取相应的操作。
9、说说 Redis 为什么快
Redis之所以快速,主要有以下几个原因:
- 内存存储:Redis将数据存储在内存中,而不是磁盘上,这使得它能够快速读取和写入数据。相比于传统的磁盘存储数据库,如MySQL,Redis能够提供更低的访问延迟。
- 单线程模型:Redis使用单线程模型处理客户端请求。虽然这听起来可能会导致性能瓶颈,但实际上,这种设计使得Redis能够避免了多线程之间的锁竞争和上下文切换的开销。此外,单线程模型还简化了Redis的实现和维护。
- 高效的数据结构:Redis支持多种数据结构,如字符串、哈希表、列表、集合和有序集合等。这些数据结构在存储和操作数据时都经过了高度优化,使得Redis能够高效地执行各种操作,如读取、写入、更新和删除等。
- 异步操作:Redis支持异步操作,即客户端可以将一些耗时的操作交给Redis后台线程处理,而不需要等待操作完成。这使得Redis能够更好地处理并发请求和高负载情况。
- 网络模型:Redis使用基于事件驱动的网络模型,通过非阻塞I/O和事件通知机制来处理网络请求。这种模型使得Redis能够高效地处理大量的并发连接和请求。
总的来说,Redis通过内存存储、单线程模型、高效的数据结构、异步操作和优化的网络模型等多种技术手段,实现了高性能和低延迟的特性。这使得Redis成为了一个快速、可靠的键值存储系统和缓存数据库。
40岁老架构师尼恩提示:Redis既是面试的绝对重点,也是面试的绝对难点,
建议大家有一个深入和详细的掌握,具体的内容请参见《尼恩Java面试宝典-专题14:Redis 面试题》PDF,该专题对Redis有一个系统化、体系化、全面化的介绍。
如果要把Redis 高并发实战写入简历,可以找尼恩指导。
10、Redis 有几种持久化方式
Redis提供了两种主要的持久化方式:RDB(Redis Database)和AOF(Append-Only File)。
一、RDB持久化:
RDB持久化是将Redis的数据以二进制文件的形式保存到磁盘上。它是通过定期执行快照操作来实现的,可以手动触发或者根据配置的规则自动触发。RDB持久化的过程中,Redis会将当前内存中的数据快照保存到一个RDB文件中,然后将该文件写入磁盘。在Redis重启时,可以通过加载RDB文件来恢复数据。
RDB持久化的优点是快速和紧凑,因为它是通过直接将内存数据写入磁盘来完成的,不需要执行额外的I/O操作。它适用于数据备份和灾难恢复。
二、AOF持久化:
AOF持久化是通过将Redis的写命令追加到一个日志文件中来实现的。Redis会将每个写命令追加到AOF文件的末尾,以此来记录数据的变化。在Redis重启时,会重新执行AOF文件中的命令来恢复数据。
AOF持久化的优点是数据的持久性更好,因为它记录了每个写操作,可以保证数据的完整性。此外,AOF文件是以文本格式保存的,易于阅读和理解。
AOF持久化有两种模式:默认模式和重写模式。默认模式下,Redis会将写命令追加到AOF文件的末尾;而重写模式下,Redis会根据当前内存中的数据生成一个新的AOF文件,用于替换旧的AOF文件。重写模式可以减小AOF文件的大小,提高读取效率。
综合来说,RDB持久化适用于快速备份和恢复数据,而AOF持久化适用于数据的持久性和完整性要求较高的场景。可以根据实际需求选择合适的持久化方式,或者同时使用两种方式来提供更好的数据保护和恢复能力。
11、Redis 挂了怎么办?
当Redis挂了时,你可以采取以下措施来解决问题:
- 检查Redis进程:首先,确保Redis进程确实已经挂掉。你可以使用命令行或者管理工具来检查Redis进程的状态。
- 查看日志文件:如果Redis挂掉,你可以查看Redis的日志文件,通常是redis-server.log文件,来获取更多的错误信息和异常情况。日志文件可以帮助你了解Redis挂掉的原因。
- 重启Redis:如果Redis挂掉了,你可以尝试重新启动Redis。使用命令行或者管理工具来启动Redis服务器进程。
- 恢复数据:如果Redis挂掉导致数据丢失,你可以尝试从备份中恢复数据。如果你使用了Redis的持久化机制(如RDB或AOF),你可以将备份文件恢复到Redis服务器上。
- 检查服务器资源:如果Redis挂掉频繁,你需要检查服务器的资源使用情况,包括内存、CPU、磁盘等。确保服务器有足够的资源来支持Redis的正常运行。
- 优化配置:根据你的需求和服务器资源情况,可以考虑优化Redis的配置文件。例如,增加最大内存限制、调整持久化机制、调整网络参数等。
- 监控和预警:为了避免Redis挂掉,你可以使用监控工具来实时监测Redis的运行状态,并设置预警机制,及时发现并解决潜在的问题。
- 寻求专业支持:如果你无法解决Redis挂掉的问题,你可以寻求专业的技术支持或咨询,他们可以帮助你分析和解决问题。
请注意,以上步骤仅提供了一般性的解决方案,具体的操作步骤可能因你的环境和情况而有所不同。在处理Redis挂掉的问题时,建议参考Redis官方文档和相关技术资源,以获得更准确和详细的指导。
12、多线程情况下,如何保证线程安全?
一、线程安全等级
之前的博客中已有所提及“线程安全”问题,一般我们常说某某类是线程安全的,某某是非线程安全的。其实线程安全并不是一个“非黑即白”单项选择题。按照“线程安全”的安全程度由强到弱来排序,我们可以将java语言中各种操作共享的数据分为以下5类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
1. 不可变
在java语言中,不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施。如final关键字修饰的数据不可修改,可靠性最高。
2. 绝对线程安全
绝对的线程安全完全满足Brian GoetZ给出的线程安全的定义,这个定义其实是很严格的,一个类要达到“不管运行时环境如何,调用者都不需要任何额外的同步措施”通常需要付出很大的代价。
3. 相对线程安全
相对线程安全就是我们通常意义上所讲的一个类是“线程安全”的。
它需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
在java语言中,大部分的线程安全类都属于相对线程安全的,例如Vector、HashTable、Collections的synchronizedCollection()方法保证的集合。
4. 线程兼容
线程兼容就是我们通常意义上所讲的一个类不是线程安全的。
线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境下可以安全地使用。Java API中大部分的类都是属于线程兼容的。如与前面的Vector和HashTable相对应的集合类ArrayList和HashMap等。
5. 线程对立
线程对立是指无论调用端是否采取了同步错误,都无法在多线程环境中并发使用的代码。由于java语言天生就具有多线程特性,线程对立这种排斥多线程的代码是很少出现的。
一个线程对立的例子是Thread类的supend()和resume()方法。如果有两个线程同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,如果并发进行的话,无论调用时是否进行了同步,目标线程都有死锁风险。正因此如此,这两个方法已经被废弃啦。
二、线程安全的实现方法
保证线程安全以是否需要同步手段分类,分为同步方案和无需同步方案。
1. 互斥同步
互斥同步是最常见的一种并发正确性保障手段。同步是指在多线程并发访问共享数据时,保证共享数据在同一时刻只被一个线程使用(同一时刻,只有一个线程在操作共享数据)。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的。
在java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码质量,这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。
此外,ReentrantLock也是通过互斥来实现同步。在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性。
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也成为阻塞同步。从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确地同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁。
2. 非阻塞同步
随着硬件指令集的发展,出现了基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采用其他的补偿措施。(最常见的补偿错误就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
非阻塞的实现CAS(compareandswap):CAS指令需要有3个操作数,分别是内存地址(在java中理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,CAS指令指令时,当且仅当V处的值符合旧预期值A时,处理器用B更新V处的值,否则它就不执行更新,但是无论是否更新了V处的值,都会返回V的旧值,上述的处理过程是一个原子操作。
CAS缺点:
- ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
- ABA问题的解决思路就是使用版本号。在变量前面追加版本号,每次变量更新的时候把版本号加一,那么A-B-A就变成了1A-2B-3C。JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
3. 无需同步方案
要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无需任何同步操作去保证正确性,因此会有一些代码天生就是线程安全的。
1)可重入代码
可重入代码(ReentrantCode)也称为纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。所有的可重入代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。
可重入代码的特点是不依赖存储在堆上的数据和公用的系统资源、用到的状态量都是由参数中传入、不调用 非可重入的方法等。
(类比:synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁)
2)线程本地存储
如果一段代码中所需的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。这样无需同步也能保证线程之间不出现数据的争用问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典的Web交互模型中的“一个请求对应一个服务器线程(Thread-per-Request)”的处理方式,这种处理方式的广泛应用使得很多Web服务器应用都可以使用线程本地存储来解决线程安全问题。
13、用过volatile 吗?它是如何保证可见性的,原理是什么呢?
在Java中,volatile
关键字用于修饰变量,以确保对该变量的读写操作具有可见性和有序性。
volatile
关键字的原理是通过禁止线程对被修饰变量的缓存操作,直接从主内存中读取和写入变量的值。当一个线程修改了volatile
变量的值时,它会立即将新值刷新到主内存中,而不是仅仅更新自己的本地缓存。其他线程在读取该变量时,会从主内存中获取最新的值,而不是使用自己的缓存。
这种机制确保了volatile
变量的可见性,即一个线程对该变量的修改对其他线程是可见的。当一个线程修改了volatile
变量的值后,其他线程在读取该变量时,会看到最新的值。
此外,volatile
关键字还可以保证操作的有序性。具体来说,volatile
变量的写操作之前的所有操作都会在写操作之前完成,而写操作之后的所有操作都会在写操作之后开始。这就确保了对volatile
变量的操作是按照预期的顺序执行的。
需要注意的是,volatile
关键字只能保证单个变量的可见性和有序性,不能保证多个变量之间的原子性操作。如果需要保证多个变量的原子性操作,可以考虑使用synchronized
关键字或者java.util.concurrent
包中的原子类。
14、谈谈你对volatile 关键字作用和原理的理解
voliate关键字的两个作用:
1、 保证变量的可见性:当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。当一个线程向被volatile关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主内存中。当一个线程用到被volatile关键字修饰的值的时候,虚拟机会强制要求它从主内存中读取。
2、 屏蔽指令重排序:指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果时正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。非常经典的例子是在单例方法中同时对字段加入voliate,就是为了防止指令重排序。
一、可见性
简单来说:当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
MESI协议:在早期的CPU中,是通过在总线加LOCK#锁的方式实现的,但是这种方式开销太大,所以Intel开发了缓存一致性协议,也就是MESI协议。
缓存一致性思路:当CPU写数据时,如果发现操作的变量时共享变量,即其他线程的工作内存也存在该变量,于是会发信号通知其他CPU该变量的内存地址无效。当其他线程需要使用这个变量时,如内存地址失效,那么它们会在主存中重新读取该值。
详细过程:
流程图:
总线嗅探机制:
嗅探机制其实就是一个监听器,回到我们刚才的流程,如果是加入MESI缓存一致性协议和总线嗅探机制之后:
(1)CPU1读取数据a=1,CPU1的缓存中都有数据a的副本,该缓存行置为(E)状态
(2)CPU2也执行读取操作,同样CPU2也有数据a=1的副本,此时总线嗅探到CPU1也有该数据,则CPU1、CPU2两个缓存行都置为(S)状态
(3)CPU1修改数据a=2,CPU1的缓存以及主内存a=2,同时CPU1的缓存行置为(S)状态,总线发出通知,CPU2的缓存行置为(I)状态
(4)CPU2再次读取a,虽然CPU2在缓存中命中数据a=1,但是发现状态为(I),因此直接丢弃该数据,去主内存获取最新数据
二、禁止重排序
volatile禁止重排序是利用内存屏障,保证有序性。
内存屏障是一组CPU指令,用于实现对内存操作的顺序限制。
Java编译器,会在生成指令系列时,在适当的位置会插入内存屏障来禁止处理器对指令的重新排序。
(1)volatile会在变量写操作的前后加入两个内存屏障,来保证前面的写指令和后面的读指令是有序的。
(2)volatile在变量的读操作后面插入两个指令,禁止后面的读指令和写指令重排序。
总结
volatile其实可以看作是轻量级的synchronized,虽然说volatile不能保证原子性,但是如果在多线程下的操作本身就是原子性操作(例如赋值操作),那么使用volatile会由于synchronized。
volatile可以适用于某个标识flag,一旦被修改了就需要被其他线程立即可见的情况。也可以修饰作为触发器的变量,一旦变量被任何一个线程修改了,就去触发执行某个操作。
volatile最适合用的场景是一个线程修改被volatile修饰的变量,其他多个线程获取这个变量的值。
当多个线程并发修改某个变量值时,必须使用synchronized来进行互斥同步。
volatile的性能:
若一个变量用volatile修饰,那么对该变量的每次读写,CPU都需要从主内存读取,性能肯定受到一定影响。
也就是说:volatile变量远离了CPU Cache,所以没那么高效。
15、聊聊分库分表,分表为什么要停服这种操作,如果不停服可以怎么做?
一、分库分表
分库分表是一种数据库水平拆分的策略,用于解决单一数据库在数据量增大或访问压力增大时的性能瓶颈问题。它将一个数据库拆分为多个子数据库(分库),并将每个子数据库中的表进一步拆分为多个子表(分表),从而实现数据的分散存储和查询负载的分摊。
二、分表为什么要停服
在进行分表操作时,通常需要停服,主要原因有两点:
- 数据迁移:分表操作涉及到将原有的表数据迁移到新的分表中,这个过程需要将数据从原表复制到新表,可能需要进行数据转换和重新分配。在这个过程中,为了保证数据的一致性和完整性,需要停止对数据库的写入操作,以免出现数据丢失或不一致的情况。
- 数据库结构变更:分表操作通常需要对数据库的结构进行修改,包括创建新的分表、调整索引、更新外键关系等。这些操作可能会导致数据库的元数据发生变化,而且在变更过程中,数据库可能无法正常处理查询请求,因此需要停服。
三、如果不停服可以怎么做
如果不想停服进行分表操作,可以考虑以下几种方式:
- 逐步迁移:可以先创建新的分表,并将新数据写入新表中,同时保持对旧表的读取操作。随着时间的推移,新表中的数据会逐渐增多,而旧表中的数据会逐渐减少。当新表中的数据足够多时,可以将旧表中的数据迁移到新表中,最终停止对旧表的读取操作。
- 数据复制:可以在不停服的情况下,将原有表的数据复制到新表中,并通过数据同步的方式保持两个表的数据一致性。在复制过程中,需要注意处理并发写入的情况,以避免数据冲突。
- 分流处理:可以通过引入中间件或代理层来实现请求的分流处理。在分表过程中,可以将部分请求路由到新表中,而将其他请求仍然路由到旧表中,以实现逐步的分表过渡。
需要注意的是,不停服进行分表操作可能会增加系统的复杂性和风险,需要谨慎评估和测试。在进行分表操作前,应该备份数据、制定详细的计划,并确保有充分的测试和回滚策略。
16、Java虚拟机中,数据类型可以分为哪几类?
在Java虚拟机中,数据类型可以分为以下几类:
- 基本数据类型(Primitive Types):Java提供了8种基本数据类型,分别是
boolean
、byte
、short
、int
、long
、float
、double
和char
。这些类型直接存储数据的值,而不是对象的引用。 - 引用数据类型(Reference Types):引用数据类型是指除了基本数据类型以外的所有类型,包括类(Class)、接口(Interface)、数组(Array)以及枚举(Enum)等。引用数据类型存储的是对象的引用,而不是对象本身的值。
- 数组类型(Array Types):数组是一种特殊的引用数据类型,可以存储多个相同类型的元素。数组可以是一维的、二维的,甚至是多维的。
- 类类型(Class Types):类类型是指通过
class
关键字定义的类。类是对象的模板,描述了对象的属性和行为。 - 接口类型(Interface Types):接口类型是指通过
interface
关键字定义的接口。接口定义了一组方法的规范,实现了接口的类必须实现这些方法。 - 枚举类型(Enum Types):枚举类型是指通过
enum
关键字定义的枚举类。枚举类表示一组具名的常量,可以有自己的方法和属性。 - 注解类型(Annotation Types):注解类型是指通过
@interface
关键字定义的注解。注解用于为程序的元素(类、方法、变量等)添加额外的元数据。
这些数据类型在Java虚拟机中都有相应的内存表示和操作方式,开发者可以根据需求选择合适的数据类型来存储和操作数据。
17、怎么理解栈、堆?堆中存什么?栈中存什么?
在计算机科学中,栈(Stack)和堆(Heap)是两个重要的内存区域,用于存储程序运行时的数据。它们具有不同的特点和用途。
一、栈(Stack):
- 栈是一种线性数据结构,采用后进先出(LIFO)的原则,即最后进入的数据最先被访问。
- 栈中存储的是局部变量、方法参数、方法调用和返回状态等。
- 栈的大小是在程序编译阶段就确定的,由编译器自动分配和释放。栈的操作速度快,分配和回收内存非常高效。
- 栈中的数据大小和生命周期都是确定的,当一个方法执行结束后,其在栈中的数据就会被立即释放。
二、堆(Heap):
- 堆是一种动态分配内存的方式,用于存储对象和数据结构。
- 堆中存储的是通过
new
关键字创建的对象、数组以及其他动态分配的数据。 - 堆的大小可以动态调整,由垃圾回收器负责管理内存的分配和释放。
- 堆中的数据大小和生命周期是不确定的,对象的生命周期由程序的逻辑决定,当没有引用指向一个对象时,该对象就会被垃圾回收器回收。
总结:
- 栈用于存储方法的调用和返回信息,以及局部变量等数据,大小和生命周期确定。
- 堆用于存储动态分配的对象和数据,大小和生命周期不确定。
需要注意的是,Java中的基本数据类型的值可以直接存储在栈上,而引用数据类型的对象则存储在堆上,栈中存储的是对象的引用。
18、为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
将堆和栈区分出来是为了更好地管理内存和支持程序的执行。
首先,栈用于存储方法调用和返回信息,以及局部变量等数据。栈的特点是具有快速的分配和释放速度,以及按照"先进后出"的原则进行操作。当一个方法被调用时,栈会为该方法分配一块内存空间,称为栈帧。栈帧中包含了方法的参数、局部变量和返回地址等信息。当方法执行完毕后,其对应的栈帧会被释放,以便其他方法使用。由于栈的管理方式简单高效,因此适合存储较小的数据和临时变量。
而堆用于存储动态分配的对象和数据。堆的特点是具有灵活的分配和释放方式,并且可以动态地调整内存空间的大小。在堆中分配的对象可以在程序的不同部分进行共享和访问。由于堆的管理方式相对复杂,需要考虑对象的生命周期、垃圾回收等问题,因此适合存储较大的数据和长期存在的对象。
总结来说,栈和堆的区分主要是为了满足不同类型数据的存储需求和内存管理的需要。栈适合存储方法调用和局部变量等临时数据,而堆适合存储动态分配的对象和长期存在的数据。
19、为什么不把基本类型放堆中呢?
将基本类型放在堆中会导致额外的内存开销和性能损失。以下是几个原因:
- 内存开销:将基本类型放在堆中会导致每个变量都需要额外的内存空间来存储对象头和其他管理信息。相比之下,将基本类型存储在栈中只需要分配固定大小的内存空间,没有额外的开销。
- 访问速度:栈的访问速度比堆更快。因为栈中的数据是按照"先进后出"的原则进行操作,所以可以通过简单的指针操作来访问和释放栈中的数据。而堆中的数据需要通过引用来访问,需要额外的寻址和解引用操作,因此访问速度相对较慢。
- 管理复杂性:将基本类型放在堆中会增加内存管理的复杂性。堆中的对象需要进行垃圾回收和内存释放等操作,而基本类型存储在栈中不需要进行这些操作,使内存管理更加简单和高效。
- 值传递:基本类型在栈中存储时采用值传递的方式,而引用类型在堆中存储时采用引用传递的方式。值传递可以避免对象的共享和副作用,而引用传递可以实现对象的共享和传递。
综上所述,将基本类型放在栈中可以减少内存开销、提高访问速度和简化内存管理,因此通常将基本类型存储在栈中。而将引用类型存储在堆中可以实现对象的动态分配和共享。
20、Java中的参数传递时传值呢?还是传引用?
在Java中,参数传递是通过值传递(pass by value)进行的。这意味着在方法调用时,实际上是将参数的值复制一份传递给方法,而不是传递参数本身。
当传递基本类型(如int、float、boolean等)时,实际上是将该值的副本传递给方法。在方法内部对参数进行修改不会影响原始的值,因为只是对副本进行的操作。
当传递引用类型(如对象、数组等)时,实际上是将引用的副本传递给方法。引用本身是一个地址值,指向实际对象在堆中的存储位置。在方法内部对引用进行修改不会影响原始的引用,但是可以通过引用访问和修改对象的属性和状态。
需要注意的是,虽然传递引用类型时传递的是引用的副本,但是这个副本仍然指向同一个对象。因此,在方法内部对对象进行修改,会影响原始对象的状态。
另外,Java中的字符串是不可变的对象,当传递字符串时,实际上是将字符串的副本传递给方法。在方法内部对字符串进行修改时,会创建一个新的字符串对象,而不会修改原始的字符串对象。
综上所述,Java中的参数传递是通过值传递进行的,无论是基本类型还是引用类型。对于基本类型,传递的是值的副本;对于引用类型,传递的是引用的副本,但仍然指向同一个对象。
21、Java中有没有指针的概念?
在Java中,存在指针的概念,但是Java中的指针被隐藏在底层,并且不允许直接访问和操作指针。相反,Java使用引用来实现对对象的操作。
在Java中,引用是指向对象的变量,它存储了对象在内存中的地址。通过引用,我们可以间接地访问和操作对象。与指针不同,Java的引用不允许进行指针运算,不能直接访问对象的内存地址。
Java的引用具有自动内存管理的特性,即垃圾回收机制。在Java中,当一个对象不再被引用时,垃圾回收机制会自动回收该对象所占用的内存空间,这样就避免了内存泄漏和悬空指针的问题。
因此,尽管Java中没有直接操作指针的概念,但通过引用的方式,Java实现了对对象的间接操作,并提供了自动内存管理的机制,使得开发人员更加方便和安全地进行编程。
22、Java中,栈的大小通过什么参数来设置?
在Java中,栈的大小可以通过虚拟机参数来设置。具体而言,可以使用以下参数来调整栈的大小:
-Xss
:该参数用于设置线程栈的大小。
例如,-Xss1m
表示将线程栈的大小设置为1MB。
请注意,栈的大小是每个线程独立设置的,因此通过调整线程栈的大小,可以控制每个线程所占用的栈空间。
需要注意的是,栈的大小设置过小可能导致栈溢出的问题,而设置过大可能会占用过多的内存资源。因此,在调整栈的大小时需要谨慎,并根据具体的应用场景和需求进行合理的设置。
另外,栈的大小还受到操作系统和硬件的限制,超过一定限制将无法设置更大的栈空间。因此,在设置栈的大小时,需要考虑到系统的限制,并进行适当的调整。
23、一个空Object对象的占多大空间?
在Java中,一个空的Object
对象占用的空间大小主要由对象头和对齐填充所占用的空间决定。
对象头包含了一些元数据信息,如对象的哈希码、锁状态、GC标记等。在64位的JVM上,对象头的大小通常为12字节。
此外,由于JVM对内存的分配和对齐要求,对象的大小必须是字节对齐的。在大多数情况下,Java对象的大小都会被自动调整为8字节的倍数。
因此,一个空的Object
对象在64位的JVM上的占用空间大小通常为16字节。这包括对象头的12字节和对齐填充的4字节。
需要注意的是,不同的JVM实现可能会有所不同,因此实际空对象的大小可能会有所差异。另外,如果在Object
对象中添加了实例变量,那么空对象的大小将会增加,具体大小取决于添加的实例变量的数量和类型。
24、对象引用类型分为哪几类?
在Java中,对象引用类型可以分为以下几类:
- 强引用(Strong Reference):强引用是最常见的引用类型。当一个对象被强引用关联时,即使内存不足时也不会被垃圾回收器回收。只有当该对象没有任何强引用时,才会被判定为可回收对象。
- 软引用(Soft Reference):软引用用于描述一些还有用但非必需的对象。当内存不足时,垃圾回收器可能会回收软引用对象。在Java中,可以使用
SoftReference
类来创建软引用。 - 弱引用(Weak Reference):弱引用比软引用更弱,用于描述非必需的对象。当垃圾回收器运行时,无论内存是否充足,都会回收弱引用对象。在Java中,可以使用
WeakReference
类来创建弱引用。 - 虚引用(Phantom Reference):虚引用是最弱的引用类型,几乎没有直接的访问价值。虚引用的主要作用是跟踪对象被垃圾回收器回收的状态。在Java中,可以使用
PhantomReference
类来创建虚引用。
这些引用类型的主要区别在于垃圾回收器对它们的处理方式不同。强引用在内存不足时也不会被回收,而软引用、弱引用和虚引用在内存不足时可能会被回收。
25、讲一讲垃圾回收算法
垃圾回收算法是自动内存管理的核心部分,它负责在运行时自动回收不再使用的对象,释放它们所占用的内存空间。垃圾回收算法主要包括以下几种:
- 引用计数算法(Reference Counting):该算法通过为每个对象维护一个引用计数器,记录对象被引用的次数。当引用计数器为0时,说明该对象不再被引用,可以被回收。然而,引用计数算法无法解决循环引用的问题,即两个或多个对象相互引用,但没有被其他对象引用。因此,在Java中并未采用引用计数算法作为主要的垃圾回收算法。
- 标记-清除算法(Mark and Sweep):标记-清除算法是Java中最常用的垃圾回收算法之一。它分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾回收器从根对象开始遍历所有可达对象,并对它们进行标记。在清除阶段,垃圾回收器清除未被标记的对象,并回收它们占用的内存空间。标记-清除算法可以有效地处理循环引用的情况,但会产生内存碎片。
- 复制算法(Copying):复制算法将可用内存空间划分为两个区域,通常称为"From"空间和"To"空间。在垃圾回收过程中,首先将所有存活的对象从"From"空间复制到"To"空间,然后清除"From"空间中的所有对象。复制算法简单高效,不会产生内存碎片,但会浪费一部分内存空间。
- 标记-压缩算法(Mark and Compact):标记-压缩算法是一种综合了标记-清除算法和复制算法的垃圾回收算法。它首先通过标记阶段标记所有存活的对象,然后将它们向一端移动,紧凑排列,最后清除边界外的内存空间。标记-压缩算法既能够处理循环引用,又能够减少内存碎片。
垃圾回收器通常会根据不同的情况选择合适的垃圾回收算法。例如,新生代使用复制算法,老年代使用标记-清除或标记-压缩算法。此外,Java还提供了不同的垃圾回收器实现,如Serial、Parallel、CMS、G1等,每个垃圾回收器都有不同的特点和适用场景。
26、如何解决内存碎片的问题?
内存碎片是指在使用动态内存分配时,内存空间被分割成多个小块,而这些小块之间存在一些未被使用的空隙。内存碎片的存在可能导致内存利用率降低,甚至造成内存分配失败。
为了解决内存碎片问题,可以采取以下几种方法:
- 内存池:使用内存池技术可以避免频繁的内存分配和释放操作,从而减少内存碎片的产生。内存池会在程序启动时预先分配一块连续的内存空间,并将其划分为多个固定大小的内存块。当需要内存时,直接从内存池中获取一个可用的内存块,而不是每次都进行动态内存分配。这样可以减少碎片的产生,提高内存利用率。
- 内存整理:内存整理是指将散乱的内存块进行整理,使得空闲的内存块连续排列。可以通过内存拷贝的方式将已分配的内存块移动到一起,从而消除碎片。这个过程需要暂停程序的执行,因此一般在空闲时间进行。
- 分配算法优化:内存分配算法也可以对内存碎片进行优化。常见的算法有首次适应、最佳适应和最坏适应算法。首次适应算法选择第一个满足要求的空闲块进行分配,最佳适应算法选择最小的满足要求的空闲块进行分配,最坏适应算法选择最大的满足要求的空闲块进行分配。不同的算法对内存碎片的影响不同,可以根据实际情况选择合适的算法。
- 压缩算法:压缩算法是一种将已分配的内存块进行压缩,使得空闲内存块连续排列的方法。通过将内存块向一端移动,可以将空闲块合并在一起,从而减少碎片。这个过程需要暂停程序的执行,因此一般在空闲时间进行。
以上是一些常见的解决内存碎片问题的方法,具体的选择可以根据实际情况和需求来确定。
27、说说JVM底层原理和排查命令
JVM(Java Virtual Machine)是Java虚拟机的缩写,是Java程序运行的基础平台。它是一个在物理机器上模拟运行Java字节码的虚拟计算机,负责解释和执行Java程序。
JVM底层原理:
- 类加载:JVM将Java源代码编译成字节码文件,然后通过类加载器将字节码文件加载到内存中。类加载器负责查找、加载和验证类文件。
- 内存管理:JVM使用内存管理器来管理内存,包括堆、栈和方法区。堆用于存储对象实例,栈用于存储局部变量和方法调用,方法区用于存储类信息和常量池。
- 垃圾回收:JVM通过垃圾回收机制自动管理内存。它会周期性地检查不再被引用的对象,并回收它们所占用的内存空间。
- 即时编译:JVM使用即时编译器将字节码转换成本地机器码,以提高程序的执行效率。
- 异常处理:JVM提供了异常处理机制,可以捕获和处理程序中的异常。
JVM排查命令:
jps
:用于列出当前运行的Java进程,显示进程ID和类名。jstat
:用于监控JVM内存、垃圾回收和类加载等信息。jmap
:用于生成堆转储快照,查看堆内存使用情况。jstack
:用于生成线程转储快照,查看线程状态和调用栈信息。jinfo
:用于查看和修改JVM的配置参数。jconsole
:图形化工具,用于监视和管理JVM。jcmd
:用于向正在运行的Java进程发送诊断命令。
这些命令可以帮助开发人员监控和调试Java应用程序,定位问题和优化性能。使用这些命令需要在命令行中输入相应的命令和参数,具体使用方法可以参考各个命令的文档或使用帮助命令(例如:jps -help)。
40岁老架构师尼恩提示:JVM既是面试的绝对重点,也是面试的绝对难点,
建议大家有一个深入和详细的掌握,具体的内容请参见《尼恩Java面试宝典-专题01:JVM面试题》PDF,该专题对JVM有一个系统化、体系化、全面化的介绍。
如果要把JVM调优实战写入简历,可以找尼恩指导。
28、说说ZK一致性原理
ZooKeeper是一个分布式协调服务,它提供了高可用性、一致性和可靠性的数据存储和访问机制。ZooKeeper的一致性原理主要基于ZAB(ZooKeeper Atomic Broadcast)协议来实现。下面是ZooKeeper一致性原理的详细叙述:
- 原子广播(Atomic Broadcast):ZooKeeper使用原子广播协议来保证数据的一致性。该协议确保了消息在ZooKeeper服务器集群中的顺序传递和一次性提交。ZAB协议分为两个阶段:广播和提交。
- 领导者选举(Leader Election):ZooKeeper集群中的服务器通过选举机制来选择一个领导者(Leader)。领导者负责处理客户端的读写请求,并将结果广播给其他服务器。选举过程中,服务器通过互相发送消息进行通信,最终选出一个具有最高编号的服务器作为领导者。
- 写操作的一致性:当客户端发送写请求给领导者时,领导者将请求转发给其他服务器,并等待大多数服务器的确认。一旦大多数服务器确认接收到写请求并成功写入数据,领导者会将写操作结果返回给客户端。这样可以保证写操作的一致性。
- 读操作的一致性:当客户端发送读请求给领导者时,领导者将请求转发给其他服务器,并等待大多数服务器的响应。一旦大多数服务器返回相同的结果,领导者将结果返回给客户端。这样可以保证读操作的一致性。
- 数据同步:ZooKeeper使用了多数派(Majority)的原则来保证数据的一致性。当一个服务器接收到写请求后,它会将数据变更写入本地存储,并将变更广播给其他服务器。其他服务器接收到变更后,会将其应用到本地存储。只有当大多数服务器都完成了数据的变更,才会认为写操作成功。
总之,ZooKeeper通过领导者选举、原子广播协议和多数派原则来保证数据的一致性。这种机制确保了在ZooKeeper集群中的所有服务器上的数据是一致的,并且可以提供高可用性和可靠性的分布式协调服务。
40岁老架构师尼恩提示:ZooKeeper既是面试的绝对重点,也是面试的绝对难点,
建议大家有一个深入和详细的掌握,具体的内容请参见《Java高并发核心编程 卷1加强版:NIO、Netty、Redis、ZooKeeper》PDF,该专题对ZooKeeper有一个系统化、体系化、全面化的介绍。
29、说说Redis数据结构、持久化、哨兵、cluster数据分片规则
Redis是一种内存数据库,它支持多种数据结构和持久化方式,并提供了哨兵和集群功能。下面是对Redis数据结构、持久化、哨兵和集群的详细叙述:
一、数据结构:
- 字符串(String):最基本的数据结构,可以存储字符串、整数和浮点数。
- 列表(List):有序的字符串列表,可以在头部或尾部添加、删除元素。
- 集合(Set):无序的唯一字符串集合,支持集合运算如交集、并集、差集。
- 哈希(Hash):键值对的无序散列表,适合存储对象。
- 有序集合(Sorted Set):有序的字符串集合,每个元素关联一个分数,可以按照分数排序。
二、持久化:
- RDB(Redis Database)持久化:将内存中的数据以二进制格式保存到磁盘上,可以通过配置定期保存快照或手动执行SAVE和BGSAVE命令。
- AOF(Append-Only File)持久化:以追加的方式将写操作日志保存到磁盘上,可以通过配置定期刷写或手动执行BGREWRITEAOF命令。
三、哨兵(Sentinel):
- 哨兵是一个独立的进程,用于监控Redis主从节点的状态。
- 当主节点出现故障时,哨兵可以自动将一个从节点升级为主节点,保证高可用性。
- 哨兵还负责监控主从节点的健康状态,并在需要时进行故障转移和故障恢复。
四、集群(Cluster):
- Redis集群是一个分布式的解决方案,可以将数据分散存储在多个节点上,提供高可用性和扩展性。
- 集群使用哈希槽(Hash Slot)来分片数据,每个节点负责处理一部分哈希槽的数据。
- 集群中的节点通过Gossip协议进行通信,实现节点之间的故障检测和信息传递。
- 客户端可以通过集群代理(Cluster Proxy)或者Redis客户端库来访问集群,集群代理会将请求转发给正确的节点。
总之,Redis提供了多种数据结构和持久化方式,可以根据不同的需求选择适合的存储方式。哨兵和集群功能可以提供高可用性和扩展性,使Redis在分布式环境中更加稳定和可靠。
40岁老架构师尼恩提示:Redis既是面试的绝对重点,也是面试的绝对难点,
建议大家有一个深入和详细的掌握,具体的内容请参见《尼恩Java面试宝典-专题14:Redis 面试题》PDF,该专题对Redis有一个系统化、体系化、全面化的介绍。
如果要把Redis高并发实战写入简历,可以找尼恩指导。
30、Kafka一致性原理,消费时的消息丢失和重复如何解决?
Kafka是一种分布式流处理平台,其设计目标之一是提供高度可靠的消息传递。Kafka的一致性原理主要基于分布式复制和日志提交机制。
Kafka通过将消息分为多个分区并在多个Broker上进行分布式复制来实现高可靠性。每个分区都有一个主Broker和若干个副本Broker,主Broker负责接收和写入消息,而副本Broker则负责复制主Broker上的消息。当主Broker发生故障时,副本Broker可以接替成为新的主Broker,确保消息的持久性和可用性。
在Kafka中,消费者可以通过消费者组的方式进行消息的消费。每个消费者组内的消费者可以并行地消费不同的分区,这样可以提高消费的吞吐量。Kafka使用偏移量(offset)来跟踪消费者在每个分区上消费的位置。消费者可以定期提交偏移量,以确保消费进度的可靠性。
关于消息丢失和重复的解决方案,Kafka提供了以下机制:
- 持久化存储:Kafka使用持久化的日志存储消息,确保消息的可靠性。即使发生故障,Kafka也可以从日志中恢复消息。
- 冗余副本:Kafka将每个分区的消息复制到多个Broker上的副本中,确保即使某个Broker发生故障,仍然可以从其他副本中获取消息。
- 偏移量提交:消费者定期提交偏移量,以确保消费进度的可靠性。如果消费者发生故障,它可以从上一次提交的偏移量处继续消费,避免消息的重复消费。
- Exactly Once语义:Kafka提供了Exactly Once语义的支持,确保消息在生产者和消费者之间的精确一次交付。这是通过事务机制和幂等性保证实现的。
通过这些机制,Kafka能够提供高度可靠的消息传递,并有效地解决消息丢失和重复的问题。
31、说说微服务优缺点
微服务架构是一种将应用程序拆分为一组小型、独立部署的服务的软件开发方法。它具有以下优点和缺点:
优点:
- 松耦合:微服务架构将应用程序拆分为多个小型服务,每个服务都是独立的,可以独立开发、部署和扩展。这种松耦合的设计使得团队可以更加独立地开发和部署不同的服务,提高了开发效率和灵活性。
- 可扩展性:由于微服务架构的服务是独立的,可以根据需求独立地扩展每个服务。这种可扩展性使得应对高并发和大规模流量变得更加容易。
- 技术多样性:微服务架构允许每个服务使用不同的技术栈和编程语言,因为每个服务都是独立的。这使得团队可以选择最适合每个服务需求的技术,提高了开发灵活性和创新性。
- 高可用性:微服务架构中的每个服务都可以进行独立部署和水平扩展,使得系统具有更高的可用性。即使一个服务发生故障,其他服务仍然可以正常工作。
缺点:
- 系统复杂性:微服务架构将应用程序拆分为多个服务,导致系统的复杂性增加。需要管理和协调多个服务之间的通信、数据一致性和服务发现等问题。
- 分布式系统的挑战:微服务架构中的每个服务都是独立的,它们之间通过网络进行通信。这带来了网络延迟、分布式事务和数据一致性等分布式系统的挑战。
- 运维复杂性:由于微服务架构中的服务数量增加,运维变得更加复杂。需要管理多个服务的部署、监控、日志和故障排查等任务。
- 团队协作成本:微服务架构要求团队具备分布式系统和服务治理的知识。团队需要协调和合作,确保每个服务的开发和部署都能够顺利进行。
综上所述,微服务架构具有灵活性、可扩展性和高可用性等优点,但也需要应对复杂性、分布式系统挑战和运维复杂性等缺点。在选择微服务架构时,需要权衡这些优缺点,并根据具体情况进行决策。
32、说说synchronizedlock的底层实现
synchronized和Lock都是Java中用于实现线程同步的机制,它们的底层实现有所不同。
一、synchronized的底层实现:
- Java中的每个对象都有一个监视器锁(也称为内置锁或对象锁),可以通过synchronized关键字来获取和释放这个锁。
- 当一个线程执行到synchronized代码块时,它会尝试获取对象的监视器锁。
- 如果该锁没有被其他线程占用,那么该线程会获取到锁,并继续执行synchronized代码块中的内容。
- 如果该锁已经被其他线程占用,那么该线程会被阻塞,直到获取到锁为止。
- 当线程执行完synchronized代码块或者发生异常时,会释放该锁。
二、Lock的底层实现:
- Lock是Java.util.concurrent包中提供的一个接口,它定义了锁的基本操作。
- Lock接口的常用实现类是ReentrantLock,它使用了一种称为AQS(AbstractQueuedSynchronizer)的同步器来实现锁的功能。
- AQS是一个用于构建锁和同步器的框架,它提供了一个队列来管理等待锁的线程,并且使用CAS(Compare and Swap)操作来实现线程的安全切换。
- 当一个线程调用Lock的lock()方法时,它会尝试获取锁。如果锁已经被其他线程占用,那么该线程会被阻塞,直到获取到锁为止。
- 与synchronized不同,Lock提供了更灵活的锁获取和释放方式,例如可以设置获取锁的超时时间,可以在不同的代码块中分别获取和释放锁等。
总的来说,synchronized和Lock都是用于实现线程同步的机制,它们的底层实现都依赖于底层的锁机制。synchronized使用的是对象的监视器锁,而Lock使用的是AQS同步器。Lock相比synchronized提供了更多的灵活性和功能,但使用起来也更加复杂。在实际开发中,选择使用哪种机制取决于具体的需求和场景。
33、说说hashmap的底层实现
HashMap是Java中常用的数据结构,它是基于哈希表(Hash Table)实现的。下面是HashMap的底层实现的详细描述:
一、数据结构:
- HashMap是由数组和链表(或红黑树)组成的。
- 数组是HashMap的主体,用于存储元素。
- 链表(或红黑树)解决哈希冲突的问题,当多个元素映射到同一个数组位置时,它们会以链表(或红黑树)的形式存储在这个位置。
二、哈希算法:
- 当向HashMap中插入一个元素时,会根据元素的哈希码(通过hashCode()方法计算得到)来确定元素在数组中的位置。
- HashMap使用哈希算法将元素的哈希码映射到数组的索引位置。
- 哈希算法的目标是使得元素在数组中分布均匀,减少哈希冲突的概率。
三、解决哈希冲突:
- 当多个元素映射到同一个数组位置时,会以链表(或红黑树)的形式存储在这个位置。
- 在JDK 8及之前,使用的是链表解决哈希冲突的问题。
- 在JDK 8之后,当链表长度超过一定阈值(默认为8)时,链表会自动转换为红黑树,以提高查找效率。
四、扩容:
- 当HashMap中元素的数量超过数组容量的75%时,会触发扩容操作。
- 扩容操作会创建一个新的数组,并将原数组中的元素重新分配到新数组中。
- 扩容操作会导致原数组中的元素重新计算哈希码和位置,以保证元素在新数组中的分布均匀。
总的来说,HashMap的底层实现是通过数组和链表(或红黑树)的组合来实现的。它使用哈希算法将元素的哈希码映射到数组的索引位置,并使用链表(或红黑树)解决哈希冲突的问题。在插入、查找和删除元素时,HashMap会根据元素的哈希码计算出数组位置,并在该位置上进行操作。当元素数量超过一定阈值时,HashMap会自动扩容,以保证元素在数组中的分布均匀。这样可以提高HashMap的插入、查找和删除操作的效率。
34、说说Java的序列化底层实现
Java的序列化是一种将对象转换为字节流的过程,使得对象可以在网络上传输或者持久化到磁盘中。Java提供了两种序列化方式:默认序列化和自定义序列化。
默认序列化是指当一个类实现了java.io.Serializable
接口时,Java会自动进行序列化和反序列化操作。在默认序列化过程中,Java会将对象的状态以字节流的形式写入到输出流中,而反序列化则是将字节流重新转换为对象的过程。
Java的默认序列化底层实现主要涉及以下几个类和接口:
java.io.ObjectOutputStream
:该类是对象输出流,用于将对象序列化为字节流。它通过调用对象的writeObject()
方法将对象的状态写入输出流中。java.io.ObjectInputStream
:该类是对象输入流,用于将字节流反序列化为对象。它通过调用对象的readObject()
方法将字节流转换为对象的状态。java.io.Serializable
接口:该接口是一个标记接口,用于标识一个类可以进行序列化。实现了该接口的类必须提供一个无参的构造方法,并且所有非瞬态(transient)的字段都会被序列化。
在序列化过程中,Java会对对象的每个字段进行递归处理。对于基本类型和字符串类型的字段,Java会直接将其写入字节流中。对于引用类型的字段,Java会将其引用的对象进行递归序列化。
自定义序列化是指通过实现java.io.Externalizable
接口来自定义对象的序列化和反序列化过程。与默认序列化不同,自定义序列化需要手动实现writeExternal()
和readExternal()
方法来控制对象的序列化和反序列化过程。
总的来说,Java的序列化底层实现主要依赖于对象输出流和对象输入流,通过将对象的状态转换为字节流进行序列化,以及将字节流转换为对象的状态进行反序列化。
35、说说MySQL的底层实现
MySQL是一种关系型数据库管理系统,其底层实现涉及多个组件和技术。
- 存储引擎:MySQL支持多个存储引擎,如InnoDB、MyISAM、Memory等。存储引擎负责数据的存储和检索。其中,InnoDB是MySQL的默认存储引擎,它支持事务、行级锁和崩溃恢复等特性,适用于高并发和高可靠性的场景。
- 文件系统:MySQL使用文件系统来管理数据文件。每个数据库对应一个文件夹,每个表对应一个或多个文件。数据文件包括表结构、索引、数据和日志等。
- 查询优化器:MySQL的查询优化器负责解析SQL语句,并生成最优的查询计划。它会根据表的统计信息和索引选择最佳的执行路径,以提高查询性能。
- 查询执行引擎:MySQL的查询执行引擎负责执行查询计划,并返回结果。不同的存储引擎有不同的查询执行引擎。例如,InnoDB使用B+树索引进行数据检索,MyISAM使用哈希索引和全文索引。
- 锁和并发控制:MySQL使用锁和并发控制机制来保证数据的一致性和并发访问的正确性。InnoDB使用行级锁来实现高并发的读写操作,并提供了多版本并发控制(MVCC)来解决读写冲突。
- 日志系统:MySQL的日志系统包括事务日志(redo log)和二进制日志(binlog)。事务日志用于崩溃恢复和事务的持久化,二进制日志用于主从复制和数据恢复。
- 缓存管理:MySQL使用缓存来提高查询性能。其中,查询缓存用于缓存查询结果,提高相同查询的响应速度。但在高并发环境下,查询缓存可能导致性能下降,因此在最新的MySQL版本中已经被废弃。
总的来说,MySQL的底层实现包括存储引擎、文件系统、查询优化器、查询执行引擎、锁和并发控制、日志系统以及缓存管理等组件和技术。这些组件和技术相互配合,使得MySQL能够提供高性能、可靠性和可扩展性的数据库服务。
尼恩提示,如果要彻底掌握mysql底层实现,可以跟着尼恩架构团队的视频《从0开始,一步一步手写mysql》,手写一个自己的mysql,做到对mysql的深入了解。
36、说说Spring IOC, AOP, MVC的底层实现大致逻辑
Spring框架是一个开源的Java企业级应用程序开发框架,它提供了一种轻量级的、非侵入式的解决方案,用于构建企业级应用程序。Spring框架的核心是Spring IOC(Inversion of Control,控制反转)容器、Spring AOP(Aspect-Oriented Programming,面向切面编程)和Spring MVC(Model-View-Controller,模型-视图-控制器)。
一、Spring IOC的底层实现逻辑:
- Spring IOC容器负责管理和组织应用程序中的对象(也称为bean)的创建和生命周期。它通过控制反转的方式,将对象的创建和依赖关系的管理交给了容器来完成。
- Spring IOC的底层实现逻辑包括:定义bean的配置元数据(通常使用XML或注解方式)、解析配置元数据、创建bean实例、处理bean之间的依赖关系、管理bean的生命周期等。
- 在应用程序启动时,Spring IOC容器会读取配置文件或注解中定义的bean信息,并根据配置信息创建相应的bean实例,并将其存储在容器中。当其他组件需要使用这些bean时,容器会通过依赖注入的方式将相关的bean注入到需要的地方。
二、Spring AOP的底层实现逻辑:
- Spring AOP是一种基于面向切面编程的技术,它通过将横切关注点(如日志、事务、安全性等)从业务逻辑中分离出来,实现了代码的模块化和复用。
- Spring AOP的底层实现逻辑包括:定义切点(指定在哪些方法上应用切面逻辑)、定义切面(包含切面逻辑的代码)、将切面织入到目标对象中等。
- 在运行时,Spring AOP通过动态代理技术,将切面逻辑织入到目标对象的方法调用中。当调用目标对象的方法时,切面逻辑会在方法的前后执行,实现横切关注点的功能。
三、Spring MVC的底层实现逻辑:
- Spring MVC是基于MVC设计模式的Web应用程序框架,它通过将应用程序的不同层(模型、视图、控制器)进行分离,实现了业务逻辑和界面展示的解耦。
- Spring MVC的底层实现逻辑包括:定义请求映射规则、处理请求和响应、调用业务逻辑处理器、渲染视图等。
- 在应用程序启动时,Spring MVC容器会初始化并加载配置信息,包括请求映射规则、视图解析器等。当接收到客户端的请求时,容器会根据请求映射规则找到对应的处理器,并调用相应的方法进行请求处理。处理器将处理结果返回给容器,容器再根据视图解析器将结果渲染成最终的视图,最后返回给客户端。
总结来说,Spring框架的底层实现逻辑包括配置元数据的解析、对象的创建和管理、依赖注入、动态代理等技术的应用。通过这些技术,Spring实现了控制反转、面向切面编程和基于MVC的Web应用程序开发。这些底层实现逻辑使得开发人员可以更加专注于业务逻辑的实现,提高了代码的可维护性和可扩展性。
37、大致说下你熟悉的框架中用到的设计模式
在Java开发中,常用的框架中使用了许多设计模式。下面是一些常见的Java框架和它们所使用的设计模式的示例:
一、Spring框架:
- 依赖注入(Dependency Injection):Spring框架使用了依赖注入设计模式,通过注入对象的方式来实现解耦和灵活性。
- 单例模式(Singleton):Spring框架中的Bean默认是单例的,通过单例模式确保在整个应用中只有一个实例。
二、Apache Kafka:
- 发布-订阅模式(Publish-Subscribe):Kafka使用发布-订阅模式来实现消息的传递和处理,生产者将消息发布到主题(Topic)上,消费者订阅主题并接收消息。
- 适配器模式(Adapter):Kafka提供了各种适配器,用于与不同的数据源进行交互,如Kafka Connect用于与外部系统进行数据交换。
三、MyBatis框架:
- 数据访问对象模式(Data Access Object,DAO):MyBatis框架使用了DAO设计模式,通过封装数据库操作,提供了简化的数据访问接口。
四、Spring Boot框架:
- 建造者模式(Builder):Spring Boot使用建造者模式来构建应用程序的配置和环境,通过链式调用的方式创建和配置对象。
- 外观模式(Facade):Spring Boot框架提供了简化的配置和自动化的功能,隐藏了底层复杂性,使开发人员可以更方便地使用和管理应用程序。
这只是一些常见的示例,实际上,Java框架中使用的设计模式还有很多,不同的框架可能会使用不同的设计模式来解决特定的问题。设计模式的使用可以提高代码的可维护性、灵活性和可扩展性,使开发过程更加高效和规范化。
38、说说项目中用到的设计模式
在常见的Java项目中,我们经常会使用以下设计模式:
- 单例模式(Singleton Pattern):用于确保一个类只有一个实例,并提供全局访问点。常见的应用场景包括数据库连接池、线程池等。
- 工厂模式(Factory Pattern):用于创建对象的接口,但将具体的实例化逻辑延迟到子类中。常见的应用场景包括日志库、数据库驱动等。
- 观察者模式(Observer Pattern):定义了对象之间的一对多依赖关系,使得当一个对象状态改变时,所有依赖它的对象都会收到通知并自动更新。常见的应用场景包括事件监听器、消息队列等。
- 适配器模式(Adapter Pattern):将一个类的接口转换成客户端所期望的另一个接口,使得原本由于接口不兼容而不能一起工作的类可以一起工作。常见的应用场景包括将不同版本的API适配到统一的接口上。
- 策略模式(Strategy Pattern):定义了一系列的算法,并将每个算法封装起来,使它们可以相互替换。常见的应用场景包括排序算法、支付方式等。
- 模板方法模式(Template Method Pattern):定义了一个算法的骨架,将一些步骤的实现延迟到子类中。常见的应用场景包括框架中的生命周期方法、算法流程等。
- 装饰器模式(Decorator Pattern):动态地给一个对象添加额外的职责,同时又不改变其接口。常见的应用场景包括IO流的包装类、日志记录等。
- 建造者模式(Builder Pattern):将一个复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。常见的应用场景包括构建复杂的数据对象、配置对象等。
- 迭代器模式(Iterator Pattern):提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。常见的应用场景包括集合类的遍历、搜索等。
- 代理模式(Proxy Pattern):为其他对象提供一种代理以控制对这个对象的访问。常见的应用场景包括远程代理、虚拟代理等。
这些设计模式都是为了解决特定的问题而提出的,并在实际项目中得到广泛应用。不同的设计模式可以根据具体的需求选择使用,以提高代码的可读性、可维护性和可扩展性。
39、说说Netty的主要组件
Netty是一个高性能的网络编程框架,它提供了一组强大的、易于使用的组件和工具,用于构建可扩展的、高性能的网络应用程序。Netty的主要组件包括:
- Channel(通道):Channel是Netty的核心抽象,它代表了一个网络连接,可以用于读取和写入数据。Channel提供了异步的、事件驱动的I/O操作。
- EventLoop(事件循环):EventLoop是Netty的事件处理机制,它负责处理所有的I/O事件和执行任务。每个Channel都绑定了一个EventLoop,用于处理该Channel上的所有事件。
- ChannelHandler(通道处理器):ChannelHandler是Netty的事件处理器,它负责处理Channel上的各种事件,例如连接建立、数据读写等。开发人员可以通过实现自定义的ChannelHandler来处理特定的业务逻辑。
- ChannelPipeline(通道管道):ChannelPipeline是ChannelHandler的容器,它负责管理和调度ChannelHandler的执行顺序。当一个事件被触发时,它会从ChannelPipeline的头部开始,依次调用每个ChannelHandler的事件处理方法。
- Codec(编解码器):Codec是Netty的编解码器,它负责将数据在网络和应用程序之间进行转换。Netty提供了一系列的编解码器,包括基于长度的解码器、字符串编解码器、对象序列化编解码器等。
- Bootstrap(引导类):Bootstrap是Netty的引导类,用于配置和启动Netty应用程序。它提供了一组方法来设置各种参数,例如事件循环组、Channel类型、ChannelHandler等。
- ChannelFuture(通道Future):ChannelFuture是Netty的异步操作的结果,它代表了一个尚未完成的操作。通过ChannelFuture,可以获取操作的结果、添加监听器等。
这些组件共同构成了Netty的核心架构,通过它们的协同工作,可以轻松地构建高性能、可扩展的网络应用程序。
40岁老架构师尼恩提示:Netty既是面试的绝对重点,也是面试的绝对难点,
建议大家有一个深入和详细的掌握,具体的内容请参见《尼恩Java面试宝典-专题25:Netty面试题》PDF,该专题对Netty有一个系统化、体系化、全面化的介绍。
如果要把Netty实战写入简历,可以找尼恩指导。
40、使用dubbo进行远程调用时消费端需要几个线程
使用Dubbo进行远程调用时,消费端需要使用多个线程来处理不同的任务。具体来说,消费端需要使用以下几个线程:
- 主线程:主线程负责接收消费端的请求,并将请求发送给提供端。主线程还负责接收提供端的响应,并将响应返回给消费端的调用方。
- IO 线程:IO 线程负责处理网络IO操作,包括发送请求和接收响应。这些线程通常使用NIO(非阻塞IO)技术,可以高效地处理大量的并发请求。
- 线程池:消费端还可以配置一个线程池,用于处理消费端的业务逻辑。当消费端收到提供端的响应后,可以将响应交给线程池中的线程进行处理。这样可以避免阻塞主线程,提高整体的并发处理能力。
需要注意的是,线程的数量可以根据具体的需求进行配置。通常情况下,可以根据消费端的负载和性能要求来确定线程池的大小。较大的线程池可以处理更多的并发请求,但也会消耗更多的系统资源。因此,需要根据实际情况进行权衡和调整。
总结起来,使用Dubbo进行远程调用时,消费端需要主线程、IO线程和一个线程池来处理请求和响应,并发处理能力可以根据实际需求进行配置
41、说说内存分配以及优化
内存分配是计算机系统中的重要环节,它涉及到如何为程序分配和管理内存资源。下面是关于内存分配和优化的详细叙述:
一、内存分配方式:
- 栈(Stack):栈是用于存储局部变量和函数调用信息的内存区域。它的分配和释放是由编译器自动完成的,具有较快的分配和释放速度,但容量较小。
- 堆(Heap):堆是用于存储动态分配的内存对象的区域。它的分配和释放由开发人员手动控制,具有较大的容量,但分配和释放速度较慢。
- 静态存储区(Static Storage):静态存储区用于存储全局变量和静态变量,它在程序启动时分配,直到程序结束才释放。
二、内存分配优化:
- 使用合适的数据结构:选择适合问题的数据结构可以减少内存的使用量。例如,使用数组代替链表可以减少指针的开销。
- 避免内存碎片:内存碎片是指内存中存在一些零散的未被使用的空间。通过使用内存池或者内存分配算法(如分配器)可以减少内存碎片的产生。
- 及时释放不再使用的内存:当不再需要某个对象时,及时释放其占用的内存,以便其他对象可以使用这些内存。
- 使用对象池:对象池是一种将多个对象预先创建并存储在内存中的技术。通过重复使用这些对象,可以减少内存分配和释放的开销。
- 压缩内存:一些压缩算法可以将内存中的数据进行压缩,从而减少内存的使用量。例如,使用哈夫曼编码或字典压缩算法。
三、内存分配的注意事项:
- 避免内存泄漏:内存泄漏指的是程序在使用完内存后未正确释放,导致内存无法再被其他对象使用。需要注意及时释放不再使用的内存,防止内存泄漏问题。
- 避免内存溢出:内存溢出指的是程序申请的内存超过了系统所能提供的内存资源。需要合理估计程序的内存使用量,并做好内存管理和优化工作,以避免内存溢出问题。
总结起来,内存分配和优化是一个综合考虑性能和资源利用的过程。通过选择合适的数据结构、及时释放内存、使用对象池等技术手段,可以提高程序的内存使用效率和性能。同时,需要注意避免内存泄漏和内存溢出等问题,保证程序的稳定性和可靠性。
42、你怎么防止优惠券有人重复刷?
要防止优惠券被重复刷,可以考虑以下几种方法:
- 限制使用次数:在优惠券的设计中,可以设置一个使用次数的限制。每次使用优惠券时,系统会检查该优惠券的使用次数是否已达到限制,如果已达到则拒绝使用。
- 绑定用户信息:将优惠券与用户进行绑定,确保每个用户只能使用一次。在用户使用优惠券时,系统会验证该用户是否已经使用过该优惠券,如果已使用则拒绝再次使用。
- 设计有效期:为优惠券设置一个有效期,确保只能在有效期内使用。在用户使用优惠券时,系统会验证当前时间是否在优惠券的有效期范围内,如果不在则拒绝使用。
- 使用唯一标识:为每个优惠券生成一个唯一的标识符,将其存储在数据库或缓存中。在用户使用优惠券时,系统会检查该标识符是否已被使用过,如果已使用则拒绝再次使用。
- 防止恶意刷券:监控系统日志,检测异常行为。例如,检测同一用户在短时间内频繁使用优惠券的情况,或者检测同一IP地址下多个用户同时使用优惠券的情况。如果发现异常行为,可以采取相应的措施,如暂时禁止该用户使用优惠券。
需要根据具体业务场景和系统架构来选择合适的防重复刷优惠券的方法,并进行合理的组合使用。
具体请参见尼恩的专题文章: 美团太狠:接口被恶刷10Wqps,怎么防?
43、有一个整型数组,数组元素不重复,数组元素先升序后
要实现一个整型数组,其中数组元素不重复且按升序排列,可以使用以下方法和原理:
- 创建一个空的整型数组,用于存储最终结果。
- 遍历给定的整型数组。
- 对于每个元素,检查它是否已经存在于结果数组中。
- 如果不存在,则将该元素插入到结果数组中的正确位置,以保持升序排列。
- 返回结果数组作为最终结果。
以下是使用Java实现的示例代码:
import java.util.Arrays;
public class SortedArray {
public static int[] createSortedArray(int[] arr) {
int[] result = new int[arr.length];
int index = 0;
for (int i = 0; i < arr.length; i++) {
if (index == 0 || arr[i] > result[index - 1]) {
result[index++] = arr[i];
}
}
return Arrays.copyOf(result, index);
}
public static void main(String[] args) {
int[] arr = {1, 3, 2, 5, 4};
int[] sortedArr = createSortedArray(arr);
System.out.println(Arrays.toString(sortedArr));
}
}
在上述代码中,我们使用createSortedArray
方法来创建一个按升序排列的整型数组。我们使用result
数组来存储最终结果,并使用index
变量来跟踪结果数组的当前索引位置。我们遍历给定的数组,如果当前元素大于结果数组中的最后一个元素,我们将其插入到结果数组中的正确位置,并递增index
。最后,我们使用Arrays.copyOf
方法将结果数组截断为实际大小,并将其返回作为最终结果。
在示例代码中,给定的数组为{1, 3, 2, 5, 4}
,最终结果为{1, 2, 3, 4, 5}
。
44、降序,找出最大值
要实现降序并找出最大值,可以使用以下步骤和方法:
- 创建一个整数数组。
- 使用循环将一组整数存储在数组中。
- 使用排序算法(如冒泡排序、选择排序或快速排序)对数组进行降序排序。
- 输出排序后的数组的第一个元素,即最大值。
以下是使用Java实现的示例代码:
import java.util.Arrays;
public class DescendingOrder {
public static void main(String[] args) {
int[] numbers = {5, 2, 9, 1, 7}; // 示例整数数组
// 使用Arrays.sort()方法对数组进行降序排序
Arrays.sort(numbers);
for (int i = 0; i < numbers.length / 2; i++) {
int temp = numbers[i];
numbers[i] = numbers[numbers.length - 1 - i];
numbers[numbers.length - 1 - i] = temp;
}
// 输出排序后的数组
System.out.println("降序排序后的数组:");
for (int number : numbers) {
System.out.print(number + " ");
}
// 输出最大值
System.out.println("\n最大值:" + numbers[0]);
}
}
该示例代码使用了Arrays类的sort()方法对数组进行降序排序。然后,通过遍历数组找到排序后的第一个元素,即最大值。最后,输出排序后的数组和最大值。
请注意,这只是一种实现方法,还有其他排序算法和技术可以实现降序排序和找出最大值。
尼恩说在最后
在尼恩的(50+)读者社群中,很多、很多小伙伴需要进大厂、拿高薪。
尼恩团队,会持续结合一些大厂的面试真题,给大家梳理一下学习路径,看看大家需要学点啥?
前面用多篇文章,给大家介绍阿里、百度、字节、滴滴的真题:
《炸裂了…京东一面索命40问,过了就50W+》
《问麻了…阿里一面索命27问,过了就60W+》
《百度狂问3小时,大厂offer到手,小伙真狠!》
《饿了么太狠:面个高级Java,抖这多硬活、狠活》
《字节狂问一小时,小伙offer到手,太狠了!》
《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》
这些真题,都会收入到 史上最全、持续升级的 PDF电子书 《尼恩Java面试宝典》。
本文收录于 《尼恩Java面试宝典》 V84版。
基本上,把尼恩的 《尼恩Java面试宝典》吃透,大厂offer很容易拿到滴。另外,下一期的 大厂面经大家有啥需求,可以发消息给尼恩。
推荐相关阅读
《从0开始,手写Redis》
《从0开始,手写MySQL事务管理器TM》
《从0开始,手写MySQL数据管理器DM》
《腾讯太狠:40亿QQ号,给1G内存,怎么去重?》
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓