三、TCP粘包,拆包
这是在看wangjingxin大佬面经的时候看到的面试题,之前对TCP粘包,拆包没什么概念,于是就简单去了解一下。
3.1什么是拆包粘包?为什么会出现?
在进行Java NIO学习时,可能会发现:如果客户端连续不断的向服务端发送数据包时,服务端接收的数据会出现两个数据包粘在一起的情况。
TCP的首部格式:
- TCP是基于字节流的,虽然应用层和TCP传输层之间的数据交互是大小不等的数据块,但是TCP把这些数据块仅仅看成一连串无结构的字节流,没有边界;
- 从TCP的帧结构也可以看出,在TCP的首部没有表示数据长度的字段
基于上面两点,在使用TCP传输数据时,才有粘包或者拆包现象发生的可能。
一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包
接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包
拆包和粘包的问题导致接收端在处理的时候会非常困难(因为无法区分一个完整的数据包)
3.2解决拆包和粘包
分包机制一般有两个通用的解决方法:
- 1,特殊字符控制
- 2,在包头首都添加数据包的长度
如果使用netty的话,就有专门的编码器和解码器解决拆包和粘包问题了。
tips:UDP没有粘包问题,但是有丢包和乱序。不完整的包是不会有的,收到的都是完全正确的包。传送的数据单位协议是UDP报文或用户数据报,发送的时候既不合并,也不拆分。
参考资料
- https://blog.csdn.net/scythe666/article/details/51996268--->TCP粘包,拆包及解决方法
- http://www.ideawu.net/blog/archives/993.html--->关于TCP粘包和拆包的终极解答
四、select、poll、epoll简单区别
NIO回顾:
在Linux下它是这样子实现I/O复用模型的:
调用select/poll/epoll
其中一个函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。
这几个函数是有些区别的,可能有的面试官会问到这三个函数究竟有什么区别:
区别如下图:
两句话总结:
select和poll
都需要轮询每个文件描述符,epoll
基于事件驱动,不用轮询select和poll
每次都需要拷贝文件描述符,epoll
不用select
最大连接数受限,epoll和poll
最大连接数不受限
tips:epoll在内核中的实现,用红黑树管理事件块
4.1通俗例子
现在3y在公司里边实习,写完的代码需要给测试测一遍。
select/poll
情况:
- 开发在写代码,此时测试挨个问所有开发者,你写好程序了没有?要测试吗?
epoll
情况:
- 开发写完代码了,告诉测试:“我写好代码了,你去测测,功能是XXX”。于是测试高高兴兴去找bug了。
其他通俗描述[1]:
一个酒吧服务员(一个线程),前面趴了一群醉汉,突然一个吼一声“倒酒”(事件),你小跑过去给他倒一杯,然后随他去吧,突然又一个要倒酒,你又过去倒上,就这样一个服务员服务好多人,有时没人喝酒,服务员处于空闲状态,可以干点别的玩玩手机。至于epoll与select,poll的区别在于后两者的场景中醉汉不说话,你要挨个问要不要酒,没时间玩手机了。io多路复用大概就是指这几个醉汉共用一个服务员。
来源:
其他通俗描述[2]:
简单举个例子(可能也不是很形象)select/poll饭店服务员(内核)告诉饭店老板(用户程序):”现在有客人结账“但是这个服务员没人明确告诉老板,哪几桌的客人结帐。老板得自儿一个一个桌子去问:请问是你要结帐?epoll饭店服务员(内核)告诉饭店老板(用户程序):”1,2,5号客人结账“老板就可以直接去1,2,5号桌收钱了
来源:
深入了解参考资料:
- https://www.cnblogs.com/Anker/p/3265058.html--->select、poll、epoll之间的区别总结[整理]
五、Java内存模型
JVM博文回顾:
之前在写JVM的时候,还一度把JVM内存结构与Java内存模型给搞混了~~~还好有热心的网友给我指出来。
JVM内存结构:
Java内存模型:
操作变量时的规则:
- Java内存模型规定了所有的变量都存储在主内存
- 线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝
- 线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量
从工作内存同步回主内存实现是通过以下的8种操作来完成:
- lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的
保证原子性的操作:
read、load、assign、use、store和write
- synchronized锁
保证有序性(重排序导致无序)的操作:
- volatile
- synchronized锁
保证可见性:
- volatile
- synchronized锁
- final
在上面也说了,有序性可以通过volatile和synchronized锁来保证,但我们一般写程序的时候不会总是关注代码的有序性的。其实,我们Java内部中有一个原则,叫做先行发生原则(happens-before)
- “先行发生”(happens-before)原则可以通过:几条规则一揽子地解决并发环境下两个操作之间是否可能存在冲突的所有问题
- 有了这些规则,并且我们的操作是在这些规则定义的范围之内。我们就可以确保,A操作肯定比B操作先发生(不会出现重排序的问题)
“先行发生”(happens-before)原则有下面这么几条:
- 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
- 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
- volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。
- 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
- 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
- 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
- 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
参考资料:
- 【深入理解JVM】:Java内存模型JMM:https://blog.csdn.net/u011080472/article/details/51337422
- Java的内存模型(1):https://www.cnblogs.com/jian0110/p/9351281.html
六、最后
本文简单整理了一下在学习中做的笔记,还有在网上遇到一些比较重要的知识点(面试题)~希望大家看完能有所收益。
参考资料:
- 《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)》
如果大家有更好的理解方式或者文章有错误的地方还请大家不吝在评论区留言,大家互相学习交流~~~