目录
- 1.osi七层模型?数据链路层是干什么的?
- 2.tcp三次握手过程,tcp报文头部的结构?里面都有什么?
- 3.讲讲超时重传和快重传,怎么等待的
- 4.项目拷打,非常细致,问我一些Linux api
- 5.秒杀逻辑怎么实现的?
- 6.怎么保证线程安全的?
- 7.怎么抗住高并发的?
- 8.数据库的处理速度慢,那你每个用户秒杀,后端都是一个一个改数据库的内容?
- 9.项目的登录逻辑?为什么要加md5?数据库里存的是明文还是密文?项目登录逻辑通常包括以下步骤:
- 10.整个项目,在用户的角度的功能分块?在后端开发的功能分块?
- 11.反射的底层原理,常用函数?
- 12.hashmap的底层数据结构?为什么以8为链表化红黑树阈值?有没有其他情况也要化红黑树?
- 13.java中的锁怎么实现的?这种功能除了synchronized关键字和lock还有什么方法能实现线程安全?
面经来源:https://www.nowcoder.com/feed/main/detail/1eff4d47b07e41cb980b5226ce776233
1.osi七层模型?数据链路层是干什么的?
OSI(Open Systems Interconnection,开放系统互连)七层模型是一个理论上的概念模型,由国际标准化组织(ISO)提出,用来描述网络通信协议应该如何工作,以便不同厂商的设备能够互相通信。这个模型将网络通信的功能划分为七个逻辑层次,每一层都负责不同的功能,并且向上一层提供服务。
数据链路层是OSI七层模型中的第二层,介于物理层和网络层之间,其主要职责包括但不限于以下几个方面:
-
帧封装:数据链路层接收来自上层(通常是网络层)的数据报文,将其添加必要的头部和尾部信息封装成一个被称为“帧”(Frame)的数据单元。
-
链路管理:负责建立、维护和释放数据链路,即在物理链路上建立临时的、点对点的数据传输通路。
-
错误检测与修正:通过添加校验码(如CRC)等机制来检测数据在传输过程中可能发生的错误,并支持一定的错误纠正能力。
-
流量控制:确保数据不会过快地发送以至于接收方来不及处理,从而避免数据丢失或拥塞。
-
寻址:在局域网环境下,数据链路层使用MAC地址(Media Access Control Address)进行帧的寻址和转发。
-
访问控制:定义如何共享信道资源,例如以太网中采用CSMA/CD(载波监听多路访问/冲突检测)机制来决定何时发送数据。
简单来说,数据链路层就是将原始的比特流(物理层的工作成果)转化为有意义的数据帧,并通过物理层提供的传输介质进行可靠的数据传输。同时,它还保证了在同一物理网络内的各个设备间数据传输的正确性和完整性。
2.tcp三次握手过程,tcp报文头部的结构?里面都有什么?
TCP(Transmission Control Protocol)三次握手是用来建立TCP连接的一个过程,以下是这个过程的简要描述:
-
第一次握手:
- 客户端(Client)发送一个SYN(Synchronize Sequence Numbers)报文给服务器(Server),这个报文中包含一个随机生成的序列号(Sequence Number),标记为SYN = 1,ACK = 0。这个序列号将作为客户端的初始序列号(ISN)。
-
第二次握手:
- 服务器接收到客户端的SYN报文后,若同意建立连接,则回复一个SYN+ACK报文,其中SYN = 1,ACK = 1。服务器同样也会生成自己的初始序列号,并在报文中携带这个序列号(服务器的ISN),同时确认客户端的序列号(即客户端ISN+1)。
-
第三次握手:
- 客户端接收到服务器的SYN+ACK报文后,发送一个ACK报文作为回应,此时SYN = 0,ACK = 1。客户端确认服务器的序列号(即服务器ISN+1),这样双方都已知彼此的初始序列号,并且已经确认了对方的接收能力和发送意愿,建立起了一条TCP连接。
TCP报文头部结构:
TCP报文头部通常包含以下字段(按顺序排列):
- 源端口(Source Port):16位,标识发送数据的应用进程端口号。
- 目的端口(Destination Port):16位,标识接收数据的应用进程端口号。
- 序列号(Sequence Number):32位,用于标识本报文段所发送的数据的第一个字节的编号。
- 确认号(Acknowledgment Number):32位,期望收到的下一个报文段的第一个字节的编号,用于确认对方的数据。
- 数据偏移/首部长度(Data Offset):4位,指出TCP报文段的首部长度(以32-bit words计算,所以最小值为5,最大值为15,对应首部长度为20字节到60字节)。
- 保留(Reserved):一般为4位,目前均为0,保留未用。
- 控制位(Control Bits):
- URG(Urgent Pointer Field significant):紧急指针有效位。
- ACK(Acknowledgment Field significant):确认序号有效位。
- PSH(Push Function):推送操作,要求接收方尽快交付给应用层。
- RST(Reset the connection):复位连接。
- SYN(Synchronize sequence numbers):同步序列号,用于建立连接过程。
- FIN(No more data from sender):结束发送,用于终止连接。
- 窗口大小(Window Size):16位,通告接收方的缓冲区大小,用于流量控制。
- 校验和(Checksum):16位,用于检查整个TCP报文段的错误。
- 紧急指针(Urgent Pointer):16位,当URG置1时,此字段有效,指出紧急数据最后一个字节的位置。
- 可选选项(Options):长度可变,最多40字节,用于携带额外的信息,如MSS(Maximum Segment Size)、时间戳等。
以上是标准的TCP报文头部结构,每个字段的具体作用都是为了实现TCP的可靠性、有序性、流量控制和拥塞控制等功能。
3.讲讲超时重传和快重传,怎么等待的
在TCP协议中,为了确保数据的可靠传输,有两种重传机制:超时重传(Timeout Retransmission)和快速重传(Fast Retransmission)。
超时重传(Timeout Retransmission)
超时重传是指TCP发送端在发送数据段后启动一个重传计时器,等待接收端对该数据段的确认(ACK)。如果在预设的时间内没有收到预期的ACK,TCP认为该数据段可能在网络中丢失,于是重新发送该数据段。超时重传时间(Retransmission Timeout, RTO)是基于估算的往返时间(Round Trip Time, RTT)以及其变化情况(如RTT偏差)来动态确定的,一般采用Jacobson提出的指数退避算法或者其他更复杂的算法来调整RTO,以适应网络状况的变化。
快速重传(Fast Retransmission)
快速重传是一种更为高效的重传机制,它不依赖于重传计时器超时,而是通过接收端的行为来判断数据是否丢失。具体来说,当接收端收到失序的数据段(即不是按序到达的数据段)时,它会立即发送一个ACK,该ACK仍然确认的是之前正确接收的最后一个数据段的序列号,而不是最新接收到的数据段的序列号。如果发送端连续收到三个或更多相同的ACK(这些ACK都确认相同的数据段),则可以推断出中间某个数据段很可能在网络中丢失,因此即使没有等到重传计时器超时,也会立刻重传那些被认为丢失的数据段。
总结一下,两者的主要区别在于触发重传的时机不同:
- 超时重传是被动等待,依据重传计时器到期来判断是否需要重传。
- 快速重传则是主动响应,基于接收端接收到多个重复ACK的情况来快速做出反应,无需等待计时器超时,减少了不必要的延迟。
4.项目拷打,非常细致,问我一些Linux api
常见的Linux系统调用API:
-
文件操作相关API:
open()
:打开或创建一个文件。read()
:从已打开的文件中读取数据。write()
:向已打开的文件写入数据。close()
:关闭一个打开的文件描述符。unlink()
:删除一个文件。rename()
:重命名或移动一个文件。mkdir()
:创建一个新的目录。rmdir()
:删除一个空目录。
-
进程控制相关API:
fork()
:创建一个新进程,复制当前进程。execve()
:执行一个新的程序,替换当前进程映像。waitpid()
:等待子进程结束并获取其状态信息。exit()
:使进程正常退出。kill()
:发送信号给指定进程。
-
内存管理相关API:
malloc()
和free()
:动态分配和释放内存空间。mmap()
:将文件或其他对象映射到进程的虚拟地址空间。munmap()
:取消内存映射区域。
-
线程操作相关API:
pthread_create()
:创建一个新的线程。pthread_join()
:等待一个线程结束。pthread_mutex_lock()
和pthread_mutex_unlock()
:对互斥锁进行加锁和解锁操作,用于多线程同步。pthread_cond_wait()
和pthread_cond_signal()
/pthread_cond_broadcast()
:条件变量的等待和唤醒操作。
-
网络编程相关API:
socket()
:创建套接字。bind()
:将套接字与本地地址关联起来。listen()
:使套接字变为监听套接字,准备接受连接请求。accept()
:从监听套接字接受一个连接请求。connect()
:尝试连接到远程套接字。send()
和recv()
:在已连接的套接字上传输和接收数据。shutdown()
和close()
:关闭套接字或终止连接。
-
信号处理相关API:
signal()
或者sigaction()
:设置信号处理器函数。raise()
或kill()
:发送信号给进程。
如果您有特定的Linux API或者某个API的详细用法需要了解,请进一步说明。
5.秒杀逻辑怎么实现的?
秒杀逻辑的实现涉及到了高并发场景下的分布式系统设计、数据库优化、缓存策略、队列服务等多种技术手段。下面是一个较为通用且经过实践检验的秒杀系统设计方案的简化概述:
-
系统架构设计:
- 使用负载均衡器分发前端请求至后端服务器集群。
- 后端服务采用无状态设计,以水平扩展应对大流量。
-
库存管理:
- 秒杀商品的库存信息存储在Redis等内存数据库中,利用其原子操作(如
INCR
、DECR
或SETNX
)来实时更新库存数量,保证并发下库存扣减的准确性。
- 秒杀商品的库存信息存储在Redis等内存数据库中,利用其原子操作(如
-
限流与排队:
- 对前端请求进行限流,比如通过令牌桶或漏桶算法控制进入系统的请求数量。
- 使用消息队列(如RabbitMQ或Kafka)异步处理下单请求,将用户的秒杀请求先放入队列,后台服务按照队列顺序逐个处理,避免直接对数据库造成压力。
-
事务与幂等性:
- 用户请求一旦被接受,后续处理应确保事务一致性,即只有在库存足够并且下单成功时才真正扣除库存。
- 设计请求ID或用户唯一标识,对同一用户或同一请求ID仅允许成功一次,防止重复下单(幂等性)。
-
数据库操作:
- 尽量减少数据库操作,只在必要时(如完成交易时)写入订单记录。
- 使用乐观锁或悲观锁策略来避免并发下的数据不一致问题。
-
预热与缓存:
- 秒杀开始前,可以提前预热缓存,确保秒杀开始时大量请求可以直接命中缓存。
- 结合缓存击穿保护措施,比如Redis的布隆过滤器,防止大量不存在的请求直接冲击数据库。
-
系统监控与熔断降级:
- 实时监控系统性能指标,如QPS、CPU、内存等,配置熔断降级策略,当系统压力过大时自动拒绝部分请求,保护系统稳定运行。
-
验证码或滑动验证:
- 在前端增加验证码机制,降低恶意点击和机器刷单的可能性。
综合以上策略,构建一个高性能、高可用的秒杀系统,确保在高并发情况下既能处理大量的用户请求,又能准确控制商品库存,同时保证系统的稳定性。在实际生产环境中,还需要结合具体的业务需求和技术栈进行定制化的设计和优化。
6.怎么保证线程安全的?
保证线程安全的方式有很多,以下是几种常用的方法:
-
使用线程安全的类:
- Java平台提供了许多内置的线程安全类,如
ConcurrentHashMap
、CopyOnWriteArrayList
、ReentrantLock
等,它们内部已经实现了线程安全的同步机制。
- Java平台提供了许多内置的线程安全类,如
-
互斥锁(Synchronized关键字或Lock接口):
- Synchronized关键字:可以修饰方法或代码块,使得在同一时刻只有一个线程能够访问被synchronized修饰的代码块或方法。
synchronized (obj) { // 线程安全的代码块 }
- Lock接口:Java 5引入了
java.util.concurrent.locks
包,提供了比synchronized
更为灵活的锁机制,如ReentrantLock
。通过lock()
和unlock()
方法手动控制锁定和解锁过程,还可以使用try-with-resources语句自动释放锁。
- Synchronized关键字:可以修饰方法或代码块,使得在同一时刻只有一个线程能够访问被synchronized修饰的代码块或方法。
-
ThreadLocal:
- ThreadLocal为每个线程提供独立的变量副本,每个线程只能看到自己修改的变量值,不影响其他线程,主要用于解决多个线程间的数据隔离问题,而非共享数据的同步问题。
-
原子操作:
- 利用Java的
java.util.concurrent.atomic
包提供的原子类(如AtomicInteger
、AtomicLong
等)进行原子级别的操作,这些操作天然具有线程安全性。
- 利用Java的
-
CopyOnWrite容器:
- 如
CopyOnWriteArrayList
和CopyOnWriteArraySet
,在修改时会创建新的数组副本,因此对于迭代操作总是安全的,适用于读多写少的场景。
- 如
-
使用数据库事务和乐观锁/悲观锁:
- 在涉及到数据库操作时,可以通过数据库事务的ACID特性保证数据一致性,结合数据库表的乐观锁或悲观锁机制来确保多线程并发更新时的数据安全。
-
非阻塞算法与CAS(Compare and Swap):
- CAS是一种无锁算法,在硬件层面支持的情况下,可以实现无锁化的并发控制,例如在
AtomicInteger
中的compareAndSet
方法。
- CAS是一种无锁算法,在硬件层面支持的情况下,可以实现无锁化的并发控制,例如在
-
设计模式:
- 单例模式的双重检查锁定(Double-Checked Locking)或多线程环境下的不可变对象设计(Immutable Objects)也可以帮助实现线程安全。
每种方法的选择取决于具体的应用场景和性能需求,设计时需权衡线程安全带来的开销与系统的并发性能要求。
7.怎么抗住高并发的?
抗住高并发主要是通过一系列系统架构优化、资源合理分配以及软件层面的技术手段来提高系统承载能力,确保在高并发场景下依然能提供稳定的服务。以下是一些关键策略:
-
系统架构优化:
-
水平扩展(Scale Out):通过增加更多的服务器节点,分散用户请求,减轻单台服务器的压力。例如,使用负载均衡器(如Nginx、HAProxy)将请求均匀分发到后端服务器群组。
-
垂直扩展(Scale Up):提升单台服务器的硬件配置,如升级CPU、增加内存容量、使用更快的硬盘等。
-
-
分布式服务与微服务架构:
- 将系统拆分成多个独立的服务模块,每个模块专注于一项业务功能,通过服务间的协同工作完成整体业务流程。这种架构易于扩展和维护,同时有利于资源的高效利用。
-
缓存策略:
- 高并发场景下,大量请求可能会集中在热点数据上。利用Redis、Memcached等缓存系统,将经常访问的数据存储在内存中,减少对数据库的访问压力。
-
数据库优化:
- 数据库读写分离:将读操作和写操作分散在不同的数据库服务器上,减轻主数据库压力。
- 分库分表:根据业务需求将大表拆分成多个小表或分布到多个数据库,分散数据库负载。
- 使用索引优化查询性能。
- 适当使用数据库中间件(如MyCat、ShardingSphere)进行数据路由和分片管理。
-
异步处理与消息队列:
- 对于非实时性的任务,使用消息队列(如RabbitMQ、Kafka)进行解耦,异步处理请求,避免同步阻塞导致的系统瓶颈。
-
HTTP CDN加速:
- 对于静态资源,使用CDN(Content Delivery Network)进行全球分发,减轻源站服务器带宽压力。
-
服务降级与熔断:
- 在系统承受过高负载时,通过服务降级或熔断机制,暂时牺牲部分非核心功能,优先保证核心服务的稳定运行。
-
优化代码和算法:
- 减少不必要的计算和IO操作,尽量使用高效的算法和数据结构。
- 使用线程池管理和调度线程,避免过多线程导致的上下文切换开销。
-
监控与预警:
- 建立完善的监控体系,实时监控系统各项指标(如CPU、内存、磁盘、网络、QPS等),及时发现问题并通过预警系统通知运维人员。
总之,抗住高并发是一个系统工程,需要从硬件设施、软件架构、数据库优化、缓存策略等多个维度综合考虑和实施解决方案。
8.数据库的处理速度慢,那你每个用户秒杀,后端都是一个一个改数据库的内容?
在高并发场景下,尤其是秒杀活动,如果后端对每个用户请求都直接修改数据库,确实会导致数据库压力骤增,处理速度变慢,甚至可能导致数据库崩溃。为了提高处理速度,避免这个问题,我们可以采取以下策略:
-
缓存处理:
- 先将商品的库存信息存放在Redis等内存数据库中,秒杀时直接在内存中扣减库存,只有在用户秒杀成功且库存充足时,再将订单相关信息写入数据库。
-
队列处理:
- 使用消息队列(如RabbitMQ、Kafka等)来异步处理下单请求。当用户发起秒杀请求时,先将请求放入队列,后台服务按照先进先出的原则逐个处理,避免所有请求同时对数据库进行操作。
-
批量处理:
- 如果存在大量相似的数据库更新操作,可以考虑将一定数量的更新操作合并成一次数据库事务,一次性提交,减少数据库事务的开销。
-
数据库优化:
- 对热点数据做合理的索引设计,减少SQL查询的复杂度。
- 使用数据库读写分离、分库分表等手段,分散数据库负载。
-
并发控制:
- 在并发更新库存等关键数据时,使用乐观锁、悲观锁等并发控制机制,确保数据的一致性。
-
服务端预处理:
- 在秒杀活动开始前,预先将部分逻辑处理完毕,如准备好待秒杀的商品数据结构,减少秒杀时的实时计算量。
-
限流与降级:
- 对前端请求进行限流,超出系统处理能力的请求可以直接返回失败,或者进入等待队列,待系统负荷降低后再处理。
- 设置服务降级策略,当系统压力过大时,暂时关闭非核心服务,保障核心服务的稳定运行。
通过上述策略的综合运用,可以在一定程度上缓解数据库的压力,提高系统处理秒杀请求的速度和稳定性。
9.项目的登录逻辑?为什么要加md5?数据库里存的是明文还是密文?项目登录逻辑通常包括以下步骤:
-
用户输入:
用户在登录界面输入用户名(如手机号、邮箱、用户名等)和密码。 -
密码加密:
密码在发送到服务器之前,通常会在客户端使用一种哈希算法(如MD5、SHA-256等)进行单向加密处理,生成密文。这样做是为了即使传输过程中数据被截获,攻击者也无法直接获取到原始明文密码。 -
服务器验证:
服务器接收到加密后的密码后,将在数据库中查找对应的用户名,并取出与之关联的已加密存储的密码(也就是密文)。 -
密码比较:
服务器使用同样的哈希算法对待验证密码进行加密处理,并将结果与数据库中存储的密文进行对比。如果两者的哈希值匹配,则认为密码正确,用户身份得以验证,进而授权登录。
至于为什么要在数据库中存储MD5加密过的密码而不是明文,原因如下:
- 安全性:直接存储明文密码意味着一旦数据库泄露,攻击者可以直接获取到用户的原始密码,威胁用户在其它网站或服务上的安全。而存储密文则极大地增加了攻击者破解密码的难度,尤其当配合盐值(Salt)或多次迭代等方式增强哈希算法的安全性时。
- 隐私保护:即使内部工作人员误操作或恶意访问,也无法轻易获取到用户的原始密码。
然而,随着计算能力的增强和彩虹表的广泛应用,单纯使用MD5加密现在已经被视为不够安全,因为它存在碰撞风险,而且针对MD5的暴力破解和彩虹表攻击效率较高。现代做法推荐使用更安全的哈希算法(如bcrypt、scrypt或argon2等)并结合盐值(salt)来加强密码存储的安全性。
10.整个项目,在用户的角度的功能分块?在后端开发的功能分块?
在软件项目开发中,从用户角度和后端开发角度对功能进行分块的目的不同,但都是为了更好地组织、管理和实施项目。下面分别从用户视角和后端开发视角来说明如何进行功能分块:
用户角度的功能分块:
-
注册与登录模块:
- 用户账户创建、邮箱验证、密码找回、第三方账号登录等。
-
个人中心模块:
- 个人信息管理、头像设置、密码修改、隐私设置、通知管理等。
-
内容浏览模块:
- 文章阅读、产品展示、视频播放、图片浏览等,根据项目类型可能包括搜索、筛选、分类等功能。
-
互动交流模块:
- 评论、点赞、分享、收藏、举报、私信、论坛讨论等社交功能。
-
购物车与订单模块(电商平台):
- 添加商品至购物车、下单流程(选择地址、支付方式等)、订单状态跟踪、退换货申请等。
-
支付模块:
- 集成多种支付方式(如支付宝、微信支付),完成订单付款流程。
-
后台管理模块(仅限管理员):
- 内容管理、用户管理、数据分析、系统设置等功能。
后端开发角度的功能分块:
-
用户认证与授权模块:
- 实现用户注册、登录、登出、权限验证等功能,可能包括JWT、OAuth等认证机制的实现。
-
数据模型与数据库模块:
- 定义数据结构,创建数据库表结构,实现CRUD(创建、读取、更新、删除)操作,事务管理等。
-
API接口模块:
- 设计RESTful API,编写接口文档,实现前后端数据交互,可能包括用户接口、商品接口、订单接口等多个子模块。
-
业务逻辑层模块:
- 处理各种业务场景下的逻辑,如购物车逻辑、订单结算逻辑、积分系统逻辑等。
-
缓存与搜索引擎模块:
- 缓存策略的设计与实现(Redis、Memcached等),搜索引擎集成(Elasticsearch等)以提升查询效率。
-
支付对接模块:
- 与第三方支付平台的接口对接,处理支付请求、回调及异步通知等。
-
任务调度与消息队列模块:
- 定时任务的安排和执行,异步处理耗时任务,例如邮件发送、报表生成等,可能涉及如RabbitMQ、Kafka等消息队列技术。
-
监控与日志模块:
- 系统性能监控、错误日志记录与分析、预警系统等。
通过这样的功能分块,项目可以根据不同的需求和阶段合理划分工作量,同时也便于团队协作和后期的维护升级。
11.反射的底层原理,常用函数?
反射的底层原理主要涉及程序运行时对类信息的动态获取和操作。在Java和Go等编程语言中,反射机制允许我们在运行时检查类、接口、字段和方法的信息,并且能动态地创建对象、调用方法、修改字段值等。以下是它们各自反射机制的一些底层原理和常用函数:
Java反射的底层原理:
-
类加载:
- Java反射基于类加载机制,当类被加载到JVM中时,虚拟机会为其生成对应的
Class
对象,它是反射操作的基础。 Class.forName()
或ClassLoader.loadClass()
方法可用于显式加载类并获取Class
对象。
- Java反射基于类加载机制,当类被加载到JVM中时,虚拟机会为其生成对应的
-
方法、字段获取:
- 通过
Class
对象,可以获取类的所有公共、私有、受保护或默认访问权限的方法和字段,分别使用getMethod()
、getDeclaredMethod()
、getField()
和getDeclaredField()
方法。
- 通过
-
实例化与调用:
- 使用
newInstance()
方法可以创建类的新实例,或者通过调用Constructor
对象的newInstance()
方法。 - 获取到
Method
或Field
对象后,可以调用invoke()
方法执行方法或通过set()
、get()
方法操作字段值。
- 使用
常用函数举例:
Java反射常用函数:
// 获取Class对象
Class<?> clazz = Class.forName("com.example.MyClass");
// 创建实例
Object instance = clazz.newInstance();
// 获取方法并调用
Method method = clazz.getMethod("myMethod", ...);
method.invoke(instance, ...);
// 获取字段并读取/设置值
Field field = clazz.getField("myField");
field.set(instance, newValue);
Object fieldValue = field.get(instance);
12.hashmap的底层数据结构?为什么以8为链表化红黑树阈值?有没有其他情况也要化红黑树?
在Java的HashMap
数据结构中,其底层数据结构从JDK 1.8开始发生了变化,具体设计为数组+链表+红黑树的形式:
-
数组:HashMap的基础容器是一个动态扩容的数组,每个数组元素被称为一个桶(bucket)。
-
链表:当两个或多个键映射到同一个数组索引位置时,形成链表结构,即所谓的“拉链法”解决哈希冲突。
-
红黑树:在JDK 1.8及更高版本中,为了优化查找效率,当链表长度超过8时,链表会自动转换为红黑树结构(Red-Black Tree)。红黑树是一种自平衡二叉查找树,能够保证查找、插入和删除等操作的时间复杂度接近O(log n),相较于链表,对于长链表的访问效率更高。
至于为什么选择链表长度为8作为转换阈值,这是因为经过权衡和实验发现,在实际应用中,这个长度既可以有效避免由于频繁冲突导致的线性查找效率低下,又可以尽量减少红黑树带来的额外空间开销以及树结构调整带来的性能损耗。同时,当红黑树节点数量下降到6个以下时,会再将其转换回链表结构,因为对于较小的节点数,链表结构在插入、删除等操作上的性能可能会优于红黑树。
除此之外,除了链表长度超过8的情况会自动转为红黑树外,还有另一种情况也会触发转换,那就是当执行扩容操作(resize)时,即使某个桶中的链表长度没有达到8,但如果它的下一个桶(因为在扩容后重新分配位置)已经是个红黑树,那么这个桶里的链表也会直接转换为红黑树,以保持一致性。这一过程发生在transfer()
方法中,用于确保扩容后所有相关节点都能按照新容量规则正确分布并保持高效的数据结构。
13.java中的锁怎么实现的?这种功能除了synchronized关键字和lock还有什么方法能实现线程安全?
在Java中,实现线程安全除了使用synchronized
关键字和java.util.concurrent.locks.Lock
接口及其实现类(如ReentrantLock
)之外,还有其他几种方式可以实现线程安全:
-
Atomic Classes(原子类):
Java并发包java.util.concurrent.atomic
提供了原子变量类,如AtomicInteger
、AtomicLong
、AtomicBoolean
、AtomicReference
等,它们利用CAS(Compare and Set)操作实现原子性更新,能够在不使用锁的情况下保证线程安全。 -
ThreadLocal:
java.lang.ThreadLocal
类可以为每个线程提供一个独立的变量副本,这样不同线程之间的数据互不影响,以此达到某种程度的线程安全。不过这并不是传统意义上的互斥访问控制,而是通过隔离变量来实现的。 -
StampedLock:
java.util.concurrent.locks.StampedLock
是Java 8中引入的一种读写锁的改进版,它可以提供乐观读、悲观写以及乐观读-悲观写的混合模式,相比于synchronized
和ReentrantLock
提供了更高的灵活性和性能潜力。 -
Semaphore(信号量):
虽然信号量主要用于控制多个线程访问特定资源的数量,但也可以间接用于实现线程安全,比如控制并发访问特定资源的线程数不超过限定值。 -
Collections.synchronizedXxx() 方法:
Java集合框架提供了一些同步包装类,如Collections.synchronizedList()
、Collections.synchronizedMap()
等,它们返回一个线程安全的包装版本,但在使用时仍需要注意线程安全问题,特别是在迭代时。 -
并发容器:
Java并发包提供了如ConcurrentHashMap
、CopyOnWriteArrayList
、ConcurrentLinkedQueue
等一系列线程安全的容器类,这些容器在内部实现中采用了高级同步技术,可以高效地在多线程环境下进行读写操作。 -
栅栏(Fence):
java.util.concurrent.CountDownLatch
、CyclicBarrier
和Phaser
等工具类,它们提供了线程间同步点的功能,虽然不是严格意义上的锁,但可以帮助协调线程执行,确保线程安全的执行流程。