以下文章来源于捡田螺的小男孩 ,作者捡田螺的小男孩
专注后端技术栈,热爱分享,热爱交朋友,热爱工作总结。毕业于华南理工大学,软件工程专业~
1. 排序链表
给你链表的头结点 head ,请将其按升序排列并返回排序后的链表 。
实例1:
输入:head = [4,2,1,3]输出:[1,2,3,4]
实例2:
输入:head = [-1,5,3,4,0]输出:[-1,0,3,4,5]
这道题可以用双指针+归并排序算法解决,主要以下四个步骤:
快慢指针法,遍历链表找到中间节点
中间节点切断链表
分别用归并排序排左右子链表
合并子链表
完整代码如下:
class Solution {public ListNode sortList(ListNode head) {//如果链表为空,或者只有一个节点,直接返回即可,不用排序if (head == null || head.next == null)return head;//快慢指针移动,以寻找到中间节点ListNode slow = head;ListNode fast = head;while(fast.next!=null && fast.next.next !=null){fast = fast.next.next;slow = slow.next;}//找到中间节点,slow节点的next指针,指向midListNode mid = slow.next;//切断链表slow.next = null;//排序左子链表ListNode left = sortList(head);//排序左子链表ListNode right = sortList(mid);//合并链表return merge(left,right);}public ListNode merge(ListNode left, ListNode right) {ListNode head = new ListNode(0);ListNode temp = head;while (left != null && right != null) {if (left.val <= right.val) {temp.next = left;left = left.next;} else {temp.next = right;right = right.next;}temp = temp.next;}if (left != null) {temp.next = left;} else if (right != null) {temp.next = right;}return head.next;}}
先复习一下相关概念:
对称加密算法:加密和解密使用相同密钥的加密算法。常见的对称加密算法有 AES、3DES、DES、RC5、RC6 等。
非对称加密算法:非对称加密算法需要两个密钥(公开密钥和私有密钥)。公钥与私钥是成对存在的,如果用公钥对数据进行加密,只有对应的私钥才能解密。主要的非对称加密算法有:RSA、Elgamal、DSA、D-H、ECC。
假设应用程序的进程发起 IO 调用,但是如果内核的数据还没准备好的话,那应用程序进程就一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次 IO 操作,称之为阻塞 IO。
如果内核数据还没准备好,可以先返回错误信息给用户进程,让它不需要等待,而是通过轮询的方式再来请求。这就是非阻塞 IO,流程图如下:
IO 多路复用之 select
应用进程通过调用 select 函数,可以同时监控多个 fd,在 select 函数监控的 fd 中,只要有任何一个数据状态准备就绪了,select 函数就会返回可读状态,这时应用进程再发起 recvfrom 请求去读取数据。
select 有几个缺点:
IO 多路复用之 epoll
为了解决 select 存在的问题,多路复用模型 epoll 诞生,它采用事件驱动来实现,流程图如下:
epoll 先通过 epoll_ctl() 来注册一个 fd(文件描述符),一旦基于某个 fd 就绪时,内核会采用回调机制,迅速激活这个 fd,当进程调用 epoll_wait() 时便得到通知。这里去掉了遍历文件描述符的坑爹操作,而是采用监听事件回调的机制。这就是 epoll 的亮点。
信号驱动 IO 不再用主动询问的方式去确认数据是否就绪,而是向内核发送一个信号(调用 sigaction 的时候建立一个 SIGIO 的信号),然后应用用户进程可以去做别的事,不用阻塞。当内核数据准备好后,再通过 SIGIO 信号通知应用进程,数据准备好后的可读状态。应用用户进程收到信号之后,立即调用 recvfrom,去读取数据。
AIO 实现了 IO 全流程的非阻塞,就是应用进程发出系统调用后,是立即返回的,但是立即返回的不是处理结果,而是表示提交成功类似的意思。等内核数据准备好,将数据拷贝到用户进程缓冲区,发送信号通知用户进程 IO 操作执行完毕。
流程如下:
Hystrix 工作流程图如下:
Hystrix 提供了两个命令对象:HystrixCommand和HystrixObservableCommand,它将代表你的一个依赖请求任务,向构造函数中传入请求依赖所需要的参数。
有四种方式执行 Hystrix 命令。分别是:
如果启用了 Hystrix 缓存,任务执行前将先判断是否有相同命令执行的缓存。如果有则直接返回包含缓存响应的 Observable;如果没有缓存的结果,但启动了缓存,将缓存本次执行结果以供后续使用。
如果回路器被打开,Hystrix 将不会执行命令,直接进入 Fallback 处理逻辑。
5) 检查线程池 / 信号量 / 队列情况
Hystrix 隔离方式有线程池隔离和信号量隔离。当使用 Hystrix 线程池时,Hystrix 默认为每个依赖服务分配 10 个线程,当 10 个线程都繁忙时,将拒绝执行命令,而是立即跳到执行 fallback 逻辑。
6) 执行具体的任务
通过 HystrixObservableCommand.construct() 或者 HystrixCommand.run() 来运行用户真正的任务。
7) 计算回路健康情况
每次开始执行 command、结束执行 command 以及发生异常等情况时,都会记录执行情况,例如:成功、失败、拒绝和超时等指标情况,会定期处理这些数据,再根据设定的条件来判断是否开启回路器。
8) 命令失败时执行 Fallback 逻辑
在命令失败时执行用户指定的 Fallback 逻辑。上图中的断路、线程池拒绝、信号量拒绝、执行执行、执行超时都会进入 Fallback 处理。
9) 返回执行结果
原始对象结果将以 Observable 形式返回,在返回给用户之前,会根据调用方式的不同做一些处理。
日常开发中,我们经常遇到这种业务场景,如外卖订单超 30 分钟未支付,则自动取消订单;用户注册成功 15 分钟后,发短信消息通知用户等等。这就是延时任务处理场景。
针对此类场景我们主要有以下几种处理方案:
为了解决并发事务存在的脏读、不可重复读、幻读等问题,数据库大叔设计了四种隔离级别。分别是读未提交,读已提交,可重复读,串行化(Serializable)。
四大隔离级别,都会存在哪些并发问题呢?
Read View 的可见性规则如下:
数据库是通过加锁实现隔离级别的,比如,你想一个人静静,不被别人打扰,你可以把自己关在房子,并在房门上加上一把锁!串行化隔离级别就是加锁实现的。但是如果频繁加锁,性能会下降。因此设计数据库的大叔想到了 MVCC。
可重复读的实现原理就是 MVCC 多版本并发控制。在一个事务范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。可重复读隔离级别,就是为了解决不可重复读问题。
查询一条记录,基于 MVCC,是怎样的流程呢?
InnoDB 实现 MVCC,是通过 Read View + Undo Log 实现的,Undo Log 保存了历史快照,Read View 可见性规则帮助判断当前版本的数据是否可见。
可重复读(RR)隔离级别,是如何解决不可重复读问题的?
假设存在事务 A 和 B,SQL 执行流程如下:
在可重复读(RR)隔离级别下,一个事务里只会获取一次 read view,都是副本共用的,从而保证每次查询的数据都是一样的。
假设当前有一张 core_user 表,插入一条初始化数据,如下:
基于 MVCC,我们来看看执行流程:
然后回到版本链,开始从版本链中挑选可见的记录:
由图可以看出,最新版本的列 name 的内容是孙权,该版本的 trx_id 值为 100。开始执行 read view 可见性规则校验:
min_limit_id(100)=<trx_id(100)<102;creator_trx_id = trx_id =100;
由此可得,trx_id=100 的这个记录,当前事务是可见的。所以查到是 name 为孙权的记录。
5. 事务 B 提交事务
6. 事务 A 再次执行查询操作,因为是 RR(可重复读)隔离级别,因此会复用老的 Read View 副本,Read View 对应的值如下:
然后再次回到版本链,从版本链中挑选可见的记录:
从图可得,最新版本的列 name 的内容是曹操,该版本的 trx_id 值为 101。开始执行 read view 可见性规则校验:
min_limit_id(100)=<trx_id(101)<max_limit_id(102);因为 m_ids{100,101} 包含 trx_id(101),并且 creator_trx_id (100) 不等于 trx_id(101),所以 trx_id=101 这个记录,对于当前事务是不可见的。
这时候呢,版本链 roll_pointer 跳到下一个版本,trx_id=100 这个记录,再次校验是否可见:
min_limit_id(100)=<trx_id(100)< max_limit_id(102);因为 m_ids{100,101} 包含 trx_id(100),并且 creator_trx_id (100) 等于 trx_id(100),所以,trx_id=100 这个记录,对于当前事务是可见的,所以两次查询结果,都是 name=孙权 的那个记录。即在可重复读(RR)隔离级别下,复用老的 Read View 副本,解决了不可重复读的问题。
虚拟内存,是虚拟出来的内存,它的核心思想就是确保每个程序拥有自己的地址空间,地址空间被分成多个块,每一块都有连续的地址空间。同时物理空间也分成多个块,块大小和虚拟地址空间的块大小一致,操作系统会自动将虚拟地址空间映射到物理地址空间,程序只需关注虚拟内存,请求的也是虚拟内存,真正使用却是物理内存。
现代操作系统使用虚拟内存,即虚拟地址取代物理地址,使用虚拟内存可以有 2 个好处:
零拷贝实现思想,就利用了虚拟内存这个点:多个虚拟内存可以指向同一个物理地址,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,这样的话,就可以减少 IO 的数据拷贝次数啦,示意图如下:
排行版的实现,一般使用 Redis 的 zset 数据类型。
zadd key score member [score member ...],zrank key member实现 demo 如下:
分布式锁,是控制分布式系统不同进程共同访问共享资源的一种锁的实现。秒杀下单、抢红包等等业务场景,都需要用到分布式锁。
我们项目中经常使用 Redis 作为分布式锁。选了 Redis 分布式锁的几种实现方法,大家来讨论下,看有没有啥问题哈。
if(jedis.setnx(key,lock_value) == 1){ //加锁expire(key,100); //设置过期时间try {do something //业务请求}catch(){}finally {jedis.del(key); //释放锁}}
如果执行完 setnx 加锁,正要执行 expire 设置过期时间时,进程 crash 掉或者要重启维护了,那这个锁就“长生不老”了,别的线程永远获取不到锁啦,所以分布式锁不能这么实现。
long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间String expiresStr = String.valueOf(expires);// 如果当前锁不存在,返回加锁成功if (jedis.setnx(key, expiresStr) == 1) {return true;}// 如果锁已经存在,获取锁的过期时间String currentValueStr = jedis.get(key);// 如果获取到的过期时间,小于系统当前时间,表示已经过期if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈)String oldValueStr = jedis.getSet(key_resource_id, expiresStr);if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁return true;}}//其他情况,均返回加锁失败return false;}
笔者看过有开发小伙伴就是这么实现分布式锁的,但是这种方案也有这些缺点:
if(jedis.set(key, lock_value, "NX", "EX", 100s) == 1){ //加锁try {do something //业务处理}catch(){}finally {jedis.del(key); //释放锁}}
这个方案可能存在这样的问题:
if(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1){ //加锁try {do something //业务处理}catch(){}finally {//判断是不是当前线程加的锁,是才释放if (uni_request_id.equals(jedis.get(key))) {jedis.del(key); //释放锁}}}
在这里,判断当前线程加的锁和释放锁是不是一个原子操作。如果调用 jedis.del() 释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。
一般也是用 lua 脚本代替。lua 脚本如下:
if redis.call('get',KEYS[1]) == ARGV[1] thenreturn redis.call('del',KEYS[1])elsereturn 0end;
这种方式比较不错了,一般情况下,已经可以使用这种实现方式。但是存在锁过期释放了,业务还没执行完的问题(实际上,估算个业务处理的时间,一般没啥问题了)。
分布式锁可能存在锁过期释放,业务没执行完的问题。有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。
当前开源框架 Redisson 就解决了这个分布式锁问题。我们一起来看下 Redisson 底层原理是怎样的吧:
只要线程一加锁成功,就会启动一个 watch dog 看门狗,它是一个后台线程,会每隔 10 秒检查一下,如果线程 1 还持有锁,那么就会不断的延长锁 key 的生存时间。因此,Redisson 就是使用 Redisson 解决了锁过期释放,业务没执行完问题。
零拷贝就是不需要将数据从一个存储区域复制到另一个存储区域。它是指在传统 IO 模型中,指 CPU 拷贝的次数为 0。它是 IO 的优化方案:
流程图如下:
从流程图可以看出,传统 IO 的读写流程,包括了 4 次上下文切换(4 次用户态和内核态的切换),4 次数据拷贝(两次 CPU 拷贝以及两次的 DMA 拷贝)。
mmap 的函数原型如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
mmap 使用了虚拟内存,可以把内核空间和用户空间的虚拟地址映射到同一个物理地址,从而减少数据拷贝次数!
mmap+write 实现的零拷贝流程如下:
可以发现,mmap+write 实现的零拷贝,I/O 发生了 4 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝。其中3次数据拷贝中,包括了 2 次 DMA 拷贝和 1 次 CPU 拷贝。
mmap 是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,所以节省了一次 CPU 拷贝‘’并且用户进程内存是虚拟的,只是映射到内核的读缓冲区,可以节省一半的内存空间。
sendfile 是 Linux2.1 内核版本后引入的一个系统调用函数,API 如下:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);sendfile 表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。
sendfile 实现的零拷贝流程如下:
可以发现,sendfile 实现的零拷贝,I/O 发生了 2 次用户空间与内核空间的上下文切换,以及 3 次数据拷贝。其中 3 次数据拷贝中,包括了 2 次 DMA 拷贝和 1 次 CPU 拷贝。
那能不能把 CPU 拷贝的次数减少到 0 次呢?
有的,即带有 DMA 收集拷贝功能的 sendfile!
Linux 2.4 版本之后,对 sendfile 做了优化升级,引入 SG-DMA 技术,其实就是对 DMA 拷贝加入了 scatter/gather 操作,它可以直接从内核空间缓冲区中将数据读取到网卡。使用这个特点搞零拷贝,即还可以多省去一次 CPU 拷贝。
sendfile+DMA scatter/gather 实现的零拷贝流程如下:
可以发现,sendfile+DMA scatter/gather 实现的零拷贝,I/O 发生了 2 次用户空间与内核空间的上下文切换,以及 2 次数据拷贝。其中 2 次数据拷贝都是包 DMA 拷贝。这就是真正的 零拷贝(Zero-copy) 技术,全程都没有通过CPU来搬运数据,所有的数据都是通过 DMA 来进行传输的。
synchronized 是 Java 中的关键字,是一种同步锁。synchronized 关键字可以作用于方法或者代码块。
一般面试时。可以这么回答:
如果 synchronized 作用于代码块,反编译可以看到两个指令:monitorenter、monitorexit。JVM 使用 monitorenter 和 monitorexit 两个指令实现同步。
如果作用 synchronized 作用于方法,反编译可以看到 ACCSYNCHRONIZED 标记。JVM 通过在方法访问标识符(flags)中加入 ACCSYNCHRONIZED 来实现同步功能。
monitor 是什么呢?操作系统的管程(monitors)是概念原理,ObjectMonitor 是它的原理实现。
在 Java 虚拟机(HotSpot)中,Monitor(管程)是由 ObjectMonitor 实现的,其主要数据结构如下:
{_header = NULL;_count = 0; // 记录个数_waiters = 0,_recursions = 0;_object = NULL;_owner = NULL;_WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet_WaitSetLock = 0 ;_Responsible = NULL ;_succ = NULL ;_cxq = NULL ;FreeNext = NULL ;_EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表_SpinFreq = 0 ;_SpinClock = 0 ;OwnerIsThread = 0 ;}
ObjectMonitor 中几个关键字段的含义如图所示:
Mark Word 是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。
重量级锁,指向互斥量的指针。其实 synchronized 是重量级锁,也就是说 Synchronized 的对象锁,Mark Word 锁标识位为 10,其中指针指向的是 Monitor 对象的起始地址。
分布式 id 生成方案主要有:
什么是雪花算法?
雪花算法是一种生成分布式全局唯一 ID 的算法,生成的 ID 称为 Snowflake IDs。这种算法由 Twitter 创建,并用于推文的 ID。
一个 Snowflake ID 有 64 位。
- EOF -
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能
点赞和在看就是最大的支持❤️