秋招复习:Java后台开发
- 粘包现象怎么处理
- LInux常用命令,如何查看磁盘容量,cpu用量,各端口号
- 快速判断一个数是不是2的n次
- 求一个数n的二进制中1的个数。
- 扩展问题二:A和B的二进制中有多少位不相同。
- n!(阶乘)末尾有多少个0
- HashMap碰撞的概率
- ReentrantLock和synchronized的区别
- jvm 里 new 对象时,堆会不会发生抢占?那怎么设计jvm的堆的线程安全?
- 用uuid做主键可以吗?为什么?
- 为什么 object的方法 notify 和wait方法必须在synchronized里使用?
- 金条分为相连的7段,给一个人发7天工资,怎么发切断次数最少
- redis 有序集合(ZSET)的底层是什么,跳表的特点有哪些,具体实现是什么。比如插入过程说一下。
- redis的IO模型,从select讲到了poll,epoll,详细将epoll。为什么epoll只拷贝一次,而另外两种需要拷贝多次。具体是通过什么机制实现的。回调,哪里体现到了回调。
- TCP 拥塞控制,流量控制说一下
- hashmap和hashtable 有什么区别?
- 简单介绍一下java的生命周期有哪些阶段吗?
- 现在数据库执行过长,如何对它进行优化?
- 说一下数据库有哪些索引类型,有什么优缺点?
- 什么是死锁?死锁产生有哪些条件?
粘包现象怎么处理
TCP有粘包现象,而UDP不会出现粘包。
TCP: 是面向连接的,面向流的。TCP的收发两端都要有成对的Socket,因此,发送端为了将更多有效的包发送出去,采用了合并优化算法(Nagle算法),将多次、间隔时间短、数据量小的数据合并为一个大的数据块,进行封包处理。 这样的包对于接收端来说,就没办法分辨,所以需要一些特殊的拆包机制。
UDP: 是无连接的,面向消息的提供高效率服务。不会使用合并优化算法。 UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。
我们连续发送三个数据包,大小分别是1k,2k ,4k,这三个数据包,都已经到达了接收端的网络堆栈中,如果使用UDP协议,不管我们使用多大的接收缓冲区去接收数据,我们必须有三次接收动作,才能够把所有的数据包接收完.而使用TCP协议,我们只要把接收的缓冲区大小设置在7k以上,我们就能够一次把所有的数据包接收下来,只需要有一次接收动作。
如何处理粘包
-
提前通知接收端要传送的包的长度
粘包问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。
不建议使用,因为程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这样会放大网络延迟带来的性能损耗 -
加分割标识符
eg. /n
{数据段01}+标识符+{数据段02}+标识符
发送端和接收端约定好一个标识符来区分不同的数据包,如果接收到了这么一个分隔符,就表示一个完整的包接收完毕。
也不建议使用,因为要发送的数据很多,数据的内容格式也有很多,可能会出现标识符不唯一的情况 -
自定义包头(建议使用)
在开始传输数据时,在包头拼上自定义的一些信息,比如前4个字节表示包的长度,5-8个字节表示传输的类型(Type:做一些业务区分),后面为实际的数据包。
参考这片文章
LInux常用命令,如何查看磁盘容量,cpu用量,各端口号
- 查看端口是否被占用
netstat -an|grep 端口号 - 根据port端口号显示具体进程号(pid)
lsof -i:端口号 - top:动态查看进程(相对于ps) 以及进程所占CPU、MEM等
P M N 按下分别会按照CPU 、MEM内存占用,以及进程号的大小倒序显示。 - uptime : 显示系统已经开机运行多久,以及1,5,15分钟的平均负载:
- 查看CPU信息 #cat /proc/cpuinfo
- 查看内存信息 #cat /proc/meminfo
- 查看硬盘分区情况: #df -lh
- du -sh [目录名]:返回该目录的大小
- du -s /* | sort -nr :查看那个目录占用空间大,然后一层层排查
快速判断一个数是不是2的n次
将2的幂次方写成二进制形式后,很容易就会发现有一个特点:二进制中只有一个1,并且1后面跟了n个0; 因此问题可以转化为判断1后面是否跟了n个0就可以了。
最快速的方法: (number & number - 1) == 0
求一个数n的二进制中1的个数。
非常巧妙地利用了以上性质,n=n&(n-1) 能移除掉n的二进制中最右边的1的性质,循环移除,直到将1全部移除,这种方法将问题的复杂度降低到只和1的个数有关系。
int Func3(int data)
{ //利用了data&(data-1)每次都能移除最右边的1,移除了多少个1,就是包含了几个1
int count = 0;
while (data)
{
data = data & (data-1);
count++;
}
return count;
}
扩展问题二:A和B的二进制中有多少位不相同。
这个问题可以分为两步:
(1)将A和B异或得到C,即C=A^B,
(2)计算C的二进制中有多少个1。
n!(阶乘)末尾有多少个0
要求得末尾为0的个数,就要先分析一下末尾的0是怎么产生的,末尾的0肯定是由2和5产生的, 一对2,5相乘产生10,这样末尾就有一个0了。按照常识,一个数中5的个数肯定是要少于2的个数,那么我们只需要求5的个数就好了。这么整个问题就变为在N!中求5的个数就行了。
lines=[]
count=int(input()) # 输入N的个数
for i in range(count):
lines.append(int(input()))
def ZerosCount(N):
"""
Count the Numner of zero in the end of the N!
"""
num=0
for k in range(1,N+1):
j=k
while(j%5==0):
num+=1
j=j/5
return num
print(list(map(ZerosCount,lines)))
HashMap碰撞的概率
假设这个Hash函数是非常好的。它的Hash值均匀地分布在值域上。
在这种情况下,对于一个输入集合生成的Hash值是非常像生成一个随机数集合。我们的问题转化为如下:
给K个随机值,非负而且小于N,他们中至少有个相等的概率是多少?,我们用1减去对立问题的结果就得到原问题的结果:
(N-1/N) (N-2/N)…*(N-(k-1)/N)
如果N远大于K。所以我们可以更加化简为:K^2/2N
ReentrantLock和synchronized的区别
ReentrantLock | synchronized |
---|---|
乐观锁 | 悲观锁 |
自旋一段时间再阻塞 | 直接阻塞。。。 |
可重入锁 | 可重入锁 |
API层面的互斥锁 | java语言的关键字,是原生语法层面的互斥,需要jvm实现 |
(非)公平锁 | 非公平锁 |
等待可中断 | 不可中断,如果Thread1不释放,Thread2将一直等待,不能被中断 |
可以同时绑定多个Condition对象 | 锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件。但如果要和多于一个的条件关联的时候,就不得不额外添加一个锁。 |
jvm 里 new 对象时,堆会不会发生抢占?那怎么设计jvm的堆的线程安全?
会,假设JVM虚拟机上,每一次new 对象时,指针就会向右移动一个对象size的距离,一个线程正在给A对象分配内存,指针还没有来的及修改,另一个为B对象分配内存的线程,引用这之前的指针指向,这就发生了抢占,也被称为指针碰撞 --> CAS解决
Thread Local Allocation Buffer,线程本地分配缓存:JVM在内存新生代Eden Space中开辟了一小块线程私有的区域TLAB(Thread-local allocation buffer)。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有,所以没有锁开销。
解决:每个线程先在自己的LTAB里面分配对象,如果对象太大或者说空间已经被用完,就在共用的那一块堆中通过CAS分配内存。
用uuid做主键可以吗?为什么?
太长,非递增导致B+tree频繁分裂等等。。。
为什么 object的方法 notify 和wait方法必须在synchronized里使用?
- 调用wait()就是释放锁,释放锁的前提是必须要先获得锁,先获得锁才能释放锁。
- notify(),notifyAll()是将锁交给含有wait()方法的线程,让其继续执行下去,如果自身没有锁,怎么叫把锁交给其他线程呢;(本质是让处于入口队列的线程竞争锁)
首先,要明白,每个对象都可以被认为是一个"监视器monitor",这个监视器由三部分组成(一个独占锁,一个入口队列,一个等待队列)。
对于对象的同步方法而言,只有拥有这个对象的独占锁才能调用这个同步方法。如果这个独占锁被其他线程占用,那么另外一个调用该同步方法的线程就会处于阻塞状态,此线程进入入口队列。
若一个拥有该独占锁的线程调用该对象同步方法的wait()方法,则该线程会释放独占锁,并加入对象的等待队列;(为什么使用wait()?希望某个变量被设置之后再执行,notify()通知变量已经被设置。)
某个线程调用notify(),notifyAll()方法是将等待队列的线程转移到入口队列,然后让他们竞争锁,所以这个调用线程本身必须拥有锁。
金条分为相连的7段,给一个人发7天工资,怎么发切断次数最少
最少应该切两次。分别把金条切成七分之一、七分之二、七分之四,共计三段。第一天,给雇员七分之一那段。第二天给七分之二那段,要回七分之一那段。第三天给七分之一那段。第四天给七分之四那段,要回七分之一和七分之二那两段。第五天给他七分之一那段。第六天给他七分之二那段,要回七分之一那段。第七天给他七分之一那段
redis 有序集合(ZSET)的底层是什么,跳表的特点有哪些,具体实现是什么。比如插入过程说一下。
redis 有序集合的底层是什么:跳表
跳表
跳表的插入查找效率为O(logN),
空间复杂度是 O(n)
空间换时间
推荐阅读
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist; //跳表
跳表中的每个节点用数据结构 zskiplistNode 表示,head 和 tail 分别指向最底层链表的头尾节点。length 表示当前跳表最底层链表有多少个节点,level 记录当前跳表最高索引层数。
zskiplistNode 结构如下:
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
redis 中的有序集合结构
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
redis 中的有序集合是由我们之前介绍过的字典加上跳表实现的,字典中保存的数据和分数 score 的映射关系,每次插入数据会从字典中查询,如果已经存在了,就不再插入,有序集合中是不允许重复数据。
查询/插入过程:
- 从最高索引层开始遍历,根据 score 找到它的前驱节点,用 update 数组进行保存
- 每一层得进行节点的插入,并计算更新 span 值
- 修改 backward 指针与 tail 指针
删除节点也是类似的,首先需要根据 score 值找到目标节点,然后断开前后节点的连接,完成节点删除。
redis的IO模型,从select讲到了poll,epoll,详细将epoll。为什么epoll只拷贝一次,而另外两种需要拷贝多次。具体是通过什么机制实现的。回调,哪里体现到了回调。
Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。
I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪,能够通知程序进行相应的操作。
推荐阅读
TCP 拥塞控制,流量控制说一下
(一)慢开始算法
发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。
慢开始算法的思路就是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。
(二)拥塞避免算法:
一开始拥塞窗口成倍增大,到一定程度,拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口按线性规律缓慢增长。
无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有按时收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。
(三)快重传算法:略
(四)快恢复算法:
快重传配合使用的还有快恢复算法
当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半(为了预防网络发生拥塞)。但是接下去并不执行慢开始算法。考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。 所以此时不执行慢开始算法,而是将cwnd设置为ssthresh减半后的值,然后执行拥塞避免算法,使cwnd缓慢增大。
hashmap和hashtable 有什么区别?
hashmap | hashtable |
---|---|
不同步 | 同步 |
继承AbstractMap类 | 继承自Dictionary类 |
允许null | key/value都不能为null |
扩容2的整数次幂 | 2n+1 |
简单介绍一下java的生命周期有哪些阶段吗?
加载,验证,准备,连接,初始化,销毁
现在数据库执行过长,如何对它进行优化?
1、查看sql是否涉及多表的联表或者子查询,如果有,看是否能进行业务拆分,相关字段冗余或者合并成临时表
2、涉及连表的查询,是否能进行分表查询,单表查询之后的结果进行字段整合
3、考虑对相对应的查询条件做索引。加快查询速度
4、针对数量大的表进行历史表分离(如交易流水表)
5、数据库主从分离,读写分离,降低读写针对同一表同时的压力,至于主从同步,mysql有自带的binlog实现 主从同步
6、explain分析sql语句,查看执行计划,分析索引是否用上,分析扫描行数等等
7、查看mysql执行日志,看看是否有其他方面的问题
说一下数据库有哪些索引类型,有什么优缺点?
普通索引、唯一索引、聚集索引、主键索引、全文索引
使用索引的优点:
- 提高数据的搜索速度
- 加快表与表之间的连接速度
- 在信息检索过程中,若使用分组及排序子句进行时,通过建立索引能有效的减少检索过程中所需的分组及排序时间,提高检索效率。
使用索引的缺点:
- 在我们建立数据库的时候,需要花费的时间去建立和维护索引,而且随着数据量的增加,需要维护它的时间也会增加。
- 在创建索引的时候会占用存储空间。
- 在我们需要修改表中的数据时,索引还需要进行动态的维护,所以对数据库的维护带来了一定的麻烦。
唯一索引:在创建唯一索引时要不能给具有相同的索引值。
主键索引:在我们给一个字段设置主键的时候,它就会自动创建主键索引,用来确保每一个值都是唯一的。
聚集索引:我们在表中添加数据的顺序,与我们创建的索引键值相同,而且一个表中只能有一个聚集索引。
普通索引:它的结构主要以B+树和哈希索引为主,主要是对数据表中的数据进行精确查找。
全文索引:它的作用是搜索数据表中的字段是不是包含我们搜索的关键字,就像搜索引擎中的模糊查询。
聚集(clustered)索引,也叫聚簇索引,innodb的聚簇索引实际上是在同一个结构中保存了btree索引和数据行。 属于聚簇表示数据行和相邻的键值紧凑地存储在一起,因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
数据行的物理顺序与列值的顺序相同,如果我们查询id比较靠后的数据,那么这行数据的地址在磁盘中的物理地址也会比较靠后。而且由于物理排列方式与聚集索引的顺序相同,所以也就只能建立一个聚集索引了。
参考阅读
什么是死锁?死锁产生有哪些条件?
互斥条件,请求与保持条件,循环等待条件,不可剥夺条件