虾皮后端一面
1.介绍一下hashMap
hashMap
是基于哈希表的Map接口实现,它使用键值对(key-value)存储数据,允许使用null键和null值;
核心原理与特点:
1.数据结构:JDK1.8之后,底层是"数组+链表+红黑树".数组是主题,链表是为了解决哈希冲突(即不同的key计算出相同的值),当链表长度超过阈值8的时候且数组长度大于64时,链表会转化为红黑树,以提高查询效率;
2.哈希函数:通过key的hashCode
()的值进行高位运算和取模运算,得到数组下标,目标是分布均匀;
3.重要参数:
容量(Capacity):数组的长度,默认是16.
负载因子(Load Factor) : 默认是0.75
4.线程安全:HashMap是线程非安全的;
2.HashMap多线程安全么,怎么实现多线程安全?
线程不安全.在多线程环境下,对HashMap进行put操作可能导致死循环(JDK1.7)之前,数据丢失或数据覆盖问题.
实现线程安全的方法:
1.使用Collections.synchronizedMap(Map)
:他会返回一个包装后的Map,所有方法都用synchronized关键字加锁,性能较差.
2.使用ConcurrentHashMap
(推荐):这是专门为高并发设计的线程安全的HashMap.
JDK1.7:采用分段锁技术,将数据分成一段一段存储,每段配一把锁,允许多线程同时访问不同段的数据,提高并发度;
JDK1.8之后:摒弃了分段锁,改用synchronized+CAS(Compare And Swap)操作来实现线程安全.锁的粒度更细,只锁住数组的单个桶(链表或者红黑树的头结点),并发性能较高.
3. 说说为什么Redis使用这么广泛
Redis的广泛使用主要源于其卓越的性能和丰富的数据结构:
1.数据存储:Redis数据主要存储在内存中,读写性能极快;
2.丰富的数据结构: 不仅支持简单的String,还支持List,Hash,Set,Sorted Set,BitMap
等,可以实现复杂业务(排行榜,好有关系等);
3.持久化机制:虽然基于内存存储,但是提供了RDB(快照)和AOF(日志)两种持久化机制,保证数据不丢失;
4.高可用和分布式:通过Redis Sentinel(哨兵)实现高可用,通过Redis Cluster集群实现数据分片和横向扩展.
5.功能丰富:支持发布订阅功能,事务,Lua脚本和过期键等功能;
4.介绍一下JMM
JMM (Java Memory Model,Java内存模型) 是一种抽象概念,它定义了Java程序中的各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量的底层细节.
JMM的核心目标是解决多线程通信中的原子性,可见性,有序性问题.
主内存:存储所有共享变量。
工作内存:每个线程都有自己的工作内存,存储该线程使用到的共享变量的副本。
JMM规定了以下操作:
线程对变量的所有操作(读、写)都必须在工作内存中进行。
不同线程之间不能直接访问对方工作内存中的变量。
线程间变量值的传递需要通过主内存来完成。
关键字(如synchronized
、volatile
)以及java.util.concurrent
包下的工具,都是JMM规则的具体实现,它们保证了多线程环境下的内存可见性、操作原子性和禁止指令重排序。
5.线程池一般如何使用?
在实际项目中,不建议使用Executors
工具类直接创建线程池(如newFixedThreadPool
,newCachedThreadPool
),因为它们内部使用的阻塞队列可能无限长(如LinkedBlockingQueue
),容易导致OOM(内存溢出)。
推荐使用ThreadPoolExecutor
构造函数手动创建,以便更精准地控制参数:
java
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, // 核心线程数(如:CPU核数+1)maximumPoolSize, // 最大线程数(如:CPU核数 * 2)keepAliveTime, // 非核心线程空闲存活时间TimeUnit.SECONDS, // 时间单位new LinkedBlockingQueue<>(capacity), // 有界阻塞队列,需指定大小new CustomThreadFactory(), // 自定义线程工厂,便于日志追踪new CustomRejectedExecutionHandler() // 自定义拒绝策略 );
根据任务类型(CPU密集型、IO密集型)来设置核心参数。
6. 线程池中的拒绝策略你了解么?
当线程池的线程数达到最大值且工作队列已满时,对新提交的任务会触发拒绝策略。JDK内置了四种:
**AbortPolicy
(默认)**:直接抛出RejectedExecutionException
异常。**CallerRunsPolicy**
:将任务回退给调用者线程(即提交任务的main
线程或其它线程)来执行。**DiscardPolicy**
:直接丢弃新任务,不做任何处理。**DiscardOldestPolicy**
:丢弃队列中最老的一个任务,然后尝试再次提交当前任务。
也可以实现RejectedExecutionHandler
接口来自定义拒绝策略,如将任务持久化到磁盘或记录日志后重试。
7.线程池中来了一个线程,阻塞队列慢了,最大线程没满,会怎么样?
这个问题的描述可能有点歧义,更精确的问法是:"当一个新的任务交给线程池时,如果当前运行的线程数小于核心线程数,会直接创建新的线程.如果等于核心线程数,且工作队列还没满时,任务会入队等待.如果工作队列已满,但当前线程数小于最大线程数,会怎么样?"
答案是:线程池会创建新的非核心线程来立即执行这个新提交的任务,而不是将其放入已满的队列中.
8.如何使用RabbitMQ实现订单超时取消功能?
利用RabbitMQ的消息TTL和死信队列(DLX).
实现步骤:
创建业务队列(
order.delay.queue
):为此队列设置TTL(例如30分钟),并指定一个死信交换机(
order.dlx.exchange
)作为其死信交换机。
创建死信交换机(
order.dlx.exchange
)和死信队列(order.cancel.queue
):并将它们绑定,路由键为order.cancel
。下单时:用户下单后,向业务队列(
order.delay.queue
)发送一条消息,该消息在30分钟后会自动过期。消息过期:30分钟后,过期的消息会被自动路由到死信交换机(
order.dlx.exchange
),再被路由到死信队列(order.cancel.queue
)。消费取消逻辑:有一个消费者监听死信队列(
order.cancel.queue
)。一旦收到消息,它就检查订单状态:如果订单仍是“未支付”状态,则执行取消订单、释放库存的逻辑。
如果订单已支付,则直接丢弃消息,不做任何操作。
优点:避免了轮询数据库,性能高,解耦性好。
9. 为什么使用rabbitMQ
?
使用RabbitMQ(消息队列)的核心目的是解耦、异步、削峰填谷。
解耦:订单系统生成订单后,只需要发送一个消息到MQ,而不需要直接调用库存系统、积分系统等。即使下游系统宕机或需要扩展,也不会影响订单系统。
异步:主流程(如下单)可以快速响应给用户,而耗时的下游操作(如发短信、更新积分)通过MQ异步处理,提升系统整体响应速度。
削峰填谷:在秒杀等瞬时高并发场景下,请求可以先涌入MQ排队,后端系统按照自己的能力匀速处理,避免系统被突发流量冲垮。